mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 09:53:22 -07:00
Merge remote-tracking branch 'refs/remotes/origin/main' into tunc-portal-direction-pairing
# Conflicts: # worlds/tunic/__init__.py # worlds/tunic/docs/en_TUNIC.md # worlds/tunic/er_data.py # worlds/tunic/er_rules.py # worlds/tunic/er_scripts.py # worlds/tunic/ladder_storage_data.py # worlds/tunic/options.py # worlds/tunic/rules.py # worlds/tunic/test/test_access.py
This commit is contained in:
7
.github/workflows/unittests.yml
vendored
7
.github/workflows/unittests.yml
vendored
@@ -37,12 +37,13 @@ jobs:
|
||||
- {version: '3.9'}
|
||||
- {version: '3.10'}
|
||||
- {version: '3.11'}
|
||||
- {version: '3.12'}
|
||||
include:
|
||||
- python: {version: '3.8'} # win7 compat
|
||||
os: windows-latest
|
||||
- python: {version: '3.11'} # current
|
||||
- python: {version: '3.12'} # current
|
||||
os: windows-latest
|
||||
- python: {version: '3.11'} # current
|
||||
- python: {version: '3.12'} # current
|
||||
os: macos-latest
|
||||
|
||||
steps:
|
||||
@@ -70,7 +71,7 @@ jobs:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
python:
|
||||
- {version: '3.11'} # current
|
||||
- {version: '3.12'} # current
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -692,17 +692,25 @@ class CollectionState():
|
||||
|
||||
def update_reachable_regions(self, player: int):
|
||||
self.stale[player] = False
|
||||
world: AutoWorld.World = self.multiworld.worlds[player]
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
queue = deque(self.blocked_connections[player])
|
||||
start = self.multiworld.get_region("Menu", player)
|
||||
start: Region = world.get_region(world.origin_region_name)
|
||||
|
||||
# init on first call - this can't be done on construction since the regions don't exist yet
|
||||
if start not in reachable_regions:
|
||||
reachable_regions.add(start)
|
||||
blocked_connections.update(start.exits)
|
||||
self.blocked_connections[player].update(start.exits)
|
||||
queue.extend(start.exits)
|
||||
|
||||
if world.explicit_indirect_conditions:
|
||||
self._update_reachable_regions_explicit_indirect_conditions(player, queue)
|
||||
else:
|
||||
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
||||
|
||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
@@ -722,6 +730,29 @@ class CollectionState():
|
||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
|
||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
new_connection: bool = True
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while new_connection:
|
||||
new_connection = False
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region in reachable_regions:
|
||||
blocked_connections.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
||||
reachable_regions.add(new_region)
|
||||
blocked_connections.remove(connection)
|
||||
blocked_connections.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
new_connection = True
|
||||
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
||||
queue.extend(blocked_connections)
|
||||
|
||||
def copy(self) -> CollectionState:
|
||||
ret = CollectionState(self.multiworld)
|
||||
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}
|
||||
@@ -1176,7 +1207,7 @@ class ItemClassification(IntFlag):
|
||||
filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
|
||||
progression = 0b0001 # Item that is logically relevant
|
||||
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
|
||||
trap = 0b0100 # detrimental or entirely useless (nothing) item
|
||||
trap = 0b0100 # detrimental item
|
||||
skip_balancing = 0b1000 # should technically never occur on its own
|
||||
# Item that is logically relevant, but progression balancing should not touch.
|
||||
# Typically currency or other counted items.
|
||||
|
||||
@@ -662,17 +662,19 @@ class CommonContext:
|
||||
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
|
||||
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
def make_gui(self) -> type:
|
||||
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
|
||||
from kvui import GameManager
|
||||
|
||||
class TextManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Text Client"
|
||||
|
||||
self.ui = TextManager(self)
|
||||
return TextManager
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system from make_gui() and start running it as self.ui_task."""
|
||||
ui_class = self.make_gui()
|
||||
self.ui = ui_class(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
def run_cli(self):
|
||||
@@ -994,7 +996,7 @@ def get_base_parser(description: typing.Optional[str] = None):
|
||||
return parser
|
||||
|
||||
|
||||
def run_as_textclient():
|
||||
def run_as_textclient(*args):
|
||||
class TextContext(CommonContext):
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
tags = CommonContext.tags | {"TextOnly"}
|
||||
@@ -1033,7 +1035,7 @@ def run_as_textclient():
|
||||
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
|
||||
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
|
||||
parser.add_argument("url", nargs="?", help="Archipelago connection url")
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable
|
||||
|
||||
if args.url:
|
||||
url = urllib.parse.urlparse(args.url)
|
||||
|
||||
98
Launcher.py
98
Launcher.py
@@ -16,10 +16,11 @@ import multiprocessing
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Callable, Sequence, Union, Optional
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
@@ -107,7 +108,81 @@ components.extend([
|
||||
])
|
||||
|
||||
|
||||
def identify(path: Union[None, str]):
|
||||
def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
url = urllib.parse.urlparse(path)
|
||||
queries = urllib.parse.parse_qs(url.query)
|
||||
launch_args = (path, *launch_args)
|
||||
client_component = None
|
||||
text_client_component = None
|
||||
if "game" in queries:
|
||||
game = queries["game"][0]
|
||||
else: # TODO around 0.6.0 - this is for pre this change webhost uri's
|
||||
game = "Archipelago"
|
||||
for component in components:
|
||||
if component.supports_uri and component.game_name == game:
|
||||
client_component = component
|
||||
elif component.display_name == "Text Client":
|
||||
text_client_component = component
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, Clock, Window
|
||||
|
||||
class Popup(App):
|
||||
timer_label: Label
|
||||
remaining_time: Optional[int]
|
||||
|
||||
def __init__(self):
|
||||
self.title = "Connect to Multiworld"
|
||||
self.icon = r"data/icon.png"
|
||||
super().__init__()
|
||||
|
||||
def build(self):
|
||||
layout = BoxLayout(orientation="vertical")
|
||||
|
||||
if client_component is None:
|
||||
self.remaining_time = 7
|
||||
label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
|
||||
f"Launching Text Client in 7 seconds...")
|
||||
self.timer_label = Label(text=label_text)
|
||||
layout.add_widget(self.timer_label)
|
||||
Clock.schedule_interval(self.update_label, 1)
|
||||
else:
|
||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||
|
||||
text_client_button = Button(
|
||||
text=text_client_component.display_name,
|
||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(text_client_button)
|
||||
|
||||
game_client_button = Button(
|
||||
text=client_component.display_name,
|
||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(game_client_button)
|
||||
|
||||
layout.add_widget(button_row)
|
||||
|
||||
return layout
|
||||
|
||||
def update_label(self, dt):
|
||||
if self.remaining_time > 1:
|
||||
# countdown the timer and string replace the number
|
||||
self.remaining_time -= 1
|
||||
self.timer_label.text = self.timer_label.text.replace(
|
||||
str(self.remaining_time + 1), str(self.remaining_time)
|
||||
)
|
||||
else:
|
||||
# our timer is finished so launch text client and close down
|
||||
run_component(text_client_component, *launch_args)
|
||||
Clock.unschedule(self.update_label)
|
||||
App.get_running_app().stop()
|
||||
Window.close()
|
||||
|
||||
Popup().run()
|
||||
|
||||
|
||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||
if path is None:
|
||||
return None, None
|
||||
for component in components:
|
||||
@@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
elif not args:
|
||||
args = {}
|
||||
|
||||
if args.get("Patch|Game|Component", None) is not None:
|
||||
file, component = identify(args["Patch|Game|Component"])
|
||||
path = args.get("Patch|Game|Component|url", None)
|
||||
if path is not None:
|
||||
if path.startswith("archipelago://"):
|
||||
handle_uri(path, args.get("args", ()))
|
||||
return
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
if component:
|
||||
args['component'] = component
|
||||
if not component:
|
||||
logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}")
|
||||
logging.warning(f"Could not identify Component responsible for {path}")
|
||||
|
||||
if args["update_settings"]:
|
||||
update_settings()
|
||||
if 'file' in args:
|
||||
if "file" in args:
|
||||
run_component(args["component"], args["file"], *args["args"])
|
||||
elif 'component' in args:
|
||||
elif "component" in args:
|
||||
run_component(args["component"], *args["args"])
|
||||
elif not args["update_settings"]:
|
||||
run_gui()
|
||||
@@ -326,8 +405,9 @@ if __name__ == '__main__':
|
||||
run_group = parser.add_argument_group("Run")
|
||||
run_group.add_argument("--update_settings", action="store_true",
|
||||
help="Update host.yaml and exit.")
|
||||
run_group.add_argument("Patch|Game|Component", type=str, nargs="?",
|
||||
help="Pass either a patch file, a generated game or the name of a component to run.")
|
||||
run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?",
|
||||
help="Pass either a patch file, a generated game, the component name to run, or a url to "
|
||||
"connect with.")
|
||||
run_group.add_argument("args", nargs="*",
|
||||
help="Arguments to pass to component.")
|
||||
main(parser.parse_args())
|
||||
|
||||
@@ -75,13 +75,13 @@ def update(yes: bool = False, force: bool = False) -> None:
|
||||
if not update_ran:
|
||||
update_ran = True
|
||||
|
||||
install_pkg_resources(yes=yes)
|
||||
import pkg_resources
|
||||
|
||||
if force:
|
||||
update_command()
|
||||
return
|
||||
|
||||
install_pkg_resources(yes=yes)
|
||||
import pkg_resources
|
||||
|
||||
prev = "" # if a line ends in \ we store here and merge later
|
||||
for req_file in requirements_files:
|
||||
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
|
||||
|
||||
@@ -67,6 +67,21 @@ def update_dict(dictionary, entries):
|
||||
return dictionary
|
||||
|
||||
|
||||
def queue_gc():
|
||||
import gc
|
||||
from threading import Thread
|
||||
|
||||
gc_thread: typing.Optional[Thread] = getattr(queue_gc, "_thread", None)
|
||||
def async_collect():
|
||||
time.sleep(2)
|
||||
setattr(queue_gc, "_thread", None)
|
||||
gc.collect()
|
||||
if not gc_thread:
|
||||
gc_thread = Thread(target=async_collect)
|
||||
setattr(queue_gc, "_thread", gc_thread)
|
||||
gc_thread.start()
|
||||
|
||||
|
||||
# functions callable on storable data on the server by clients
|
||||
modify_functions = {
|
||||
# generic:
|
||||
@@ -551,6 +566,9 @@ class Context:
|
||||
self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.")
|
||||
else:
|
||||
self.save_dirty = False
|
||||
if not atexit_save: # if atexit is used, that keeps a reference anyway
|
||||
queue_gc()
|
||||
|
||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||
self.auto_saver_thread.start()
|
||||
|
||||
@@ -1203,6 +1221,10 @@ class CommonCommandProcessor(CommandProcessor):
|
||||
timer = int(seconds, 10)
|
||||
except ValueError:
|
||||
timer = 10
|
||||
else:
|
||||
if timer > 60 * 60:
|
||||
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
|
||||
|
||||
async_start(countdown(self.ctx, timer))
|
||||
return True
|
||||
|
||||
@@ -2039,6 +2061,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
item_name, usable, response = get_intended_text(item_name, names)
|
||||
if usable:
|
||||
amount: int = int(amount)
|
||||
if amount > 100:
|
||||
raise ValueError(f"{amount} is invalid. Maximum is 100.")
|
||||
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
|
||||
send_items_to(self.ctx, team, slot, *new_items)
|
||||
|
||||
|
||||
10
WebHost.py
10
WebHost.py
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
import os
|
||||
import multiprocessing
|
||||
import logging
|
||||
@@ -31,6 +32,15 @@ def get_app() -> "Flask":
|
||||
import yaml
|
||||
app.config.from_file(configpath, yaml.safe_load)
|
||||
logging.info(f"Updated config from {configpath}")
|
||||
# inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it.
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--config_override', default=None,
|
||||
help="Path to yaml config file that overrules config.yaml.")
|
||||
args = parser.parse_known_args()[0]
|
||||
if args.config_override:
|
||||
import yaml
|
||||
app.config.from_file(os.path.abspath(args.config_override), yaml.safe_load)
|
||||
logging.info(f"Updated config from {args.config_override}")
|
||||
if not app.config["HOST_ADDRESS"]:
|
||||
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
|
||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||
|
||||
@@ -72,6 +72,14 @@ class WebHostContext(Context):
|
||||
self.video = {}
|
||||
self.tags = ["AP", "WebHost"]
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
import psutil
|
||||
from Utils import format_SI_prefix
|
||||
self.logger.debug(f"Context destroyed, Mem: {format_SI_prefix(psutil.Process().memory_info().rss, 1024)}iB")
|
||||
except ImportError:
|
||||
self.logger.debug("Context destroyed")
|
||||
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
# NOTE: attributes are mutable and shared, so they will have to be copied before being modified
|
||||
@@ -249,6 +257,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
ctx = WebHostContext(static_server_data, logger)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
assert ctx.server is None
|
||||
try:
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
@@ -279,6 +288,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||
if ctx.saving:
|
||||
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
|
||||
assert ctx.shutdown_task is None
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
await ctx.shutdown_task
|
||||
|
||||
@@ -325,7 +335,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
def run(self):
|
||||
while 1:
|
||||
next_room = rooms_to_run.get(block=True, timeout=None)
|
||||
gc.collect(0)
|
||||
gc.collect()
|
||||
task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop)
|
||||
self._tasks.append(task)
|
||||
task.add_done_callback(self._done)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
flask>=3.0.3
|
||||
werkzeug>=3.0.3
|
||||
pony>=0.7.17
|
||||
werkzeug>=3.0.4
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.0
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.7.0
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.4.1; python_version >= '3.9'
|
||||
bokeh>=3.4.3; python_version == '3.9'
|
||||
bokeh>=3.5.2; python_version >= '3.10'
|
||||
markupsafe>=2.1.5
|
||||
|
||||
@@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder:
|
||||
You may also find developer documentation in the `docs` folder:
|
||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||
|
||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
||||
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
<tr>
|
||||
<td>{{ patch.player_id }}</td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td data-tooltip="Connect via Game Client"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}?game={{ patch.game }}&room={{ room.id | suuid }}">{{ patch.player_name }}</a></td>
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
You have no generated any seeds yet!
|
||||
You have not generated any seeds yet!
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,9 +118,6 @@
|
||||
# Noita
|
||||
/worlds/noita/ @ScipioWright @heinermann
|
||||
|
||||
# Ocarina of Time
|
||||
/worlds/oot/ @espeon65536
|
||||
|
||||
# Old School Runescape
|
||||
/worlds/osrs @digiholic
|
||||
|
||||
@@ -230,6 +227,9 @@
|
||||
# Links Awakening DX
|
||||
# /worlds/ladx/
|
||||
|
||||
# Ocarina of Time
|
||||
# /worlds/oot/
|
||||
|
||||
## Disabled Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||
|
||||
@@ -24,7 +24,7 @@ display as `Value1` on the webhost.
|
||||
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
|
||||
Choice, and defining `alias_true = option_full`.
|
||||
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
|
||||
`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
||||
`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
|
||||
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
|
||||
implement it for additional option types.
|
||||
@@ -129,6 +129,23 @@ class Difficulty(Choice):
|
||||
default = 1
|
||||
```
|
||||
|
||||
### Option Visibility
|
||||
Every option has a Visibility IntFlag, defaulting to `all` (`0b1111`). This lets you choose where the option will be
|
||||
displayed. This only impacts where options are displayed, not how they can be used. Hidden options are still valid
|
||||
options in a yaml. The flags are as follows:
|
||||
* `none` (`0b0000`): This option is not shown anywhere
|
||||
* `template` (`0b0001`): This option shows up in template yamls
|
||||
* `simple_ui` (`0b0010`): This option shows up on the options page
|
||||
* `complex_ui` (`0b0100`): This option shows up on the advanced/weighted options page
|
||||
* `spoiler` (`0b1000`): This option shows up in spoiler logs
|
||||
|
||||
```python
|
||||
from Options import Choice, Visibility
|
||||
|
||||
class HiddenChoiceOption(Choice):
|
||||
visibility = Visibility.none
|
||||
```
|
||||
|
||||
### Option Groups
|
||||
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
|
||||
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment
|
||||
|
||||
@@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* **Python 3.12 is currently unsupported**
|
||||
* Python 3.12.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
* possibly optional, read operating system specific sections
|
||||
@@ -31,14 +31,14 @@ After this, you should be able to run the programs.
|
||||
|
||||
Recommended steps
|
||||
* Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads)
|
||||
* **Python 3.12 is currently unsupported**
|
||||
* [read above](#General) which versions are supported
|
||||
|
||||
* **Optional**: Download and install Visual Studio Build Tools from
|
||||
[Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
|
||||
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
|
||||
Generally, selecting the box for "Desktop Development with C++" will provide what you need.
|
||||
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
|
||||
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
|
||||
|
||||
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
|
||||
* Run Generate.py which will prompt installation of missing modules, press enter to confirm
|
||||
|
||||
@@ -303,6 +303,31 @@ generation (entrance randomization).
|
||||
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
|
||||
(items that have been collected).
|
||||
|
||||
The two possible ways to make a [CollectionRule](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10) are:
|
||||
- `def rule(state: CollectionState) -> bool:`
|
||||
- `lambda state: ... boolean expression ...`
|
||||
|
||||
An access rule can be assigned through `set_rule(location, rule)`.
|
||||
|
||||
Access rules usually check for one of two things.
|
||||
- Items that have been collected (e.g. `state.has("Sword", player)`)
|
||||
- Locations, Regions or Entrances that have been reached (e.g. `state.can_reach_region("Boss Room")`)
|
||||
|
||||
Keep in mind that entrances and locations implicitly check for the accessibility of their parent region, so you do not need to check explicitly for it.
|
||||
|
||||
#### An important note on Entrance access rules:
|
||||
When using `state.can_reach` within an entrance access condition, you must also use `multiworld.register_indirect_condition`.
|
||||
|
||||
For efficiency reasons, every time reachable regions are searched, every entrance is only checked once in a somewhat non-deterministic order.
|
||||
This is fine when checking for items using `state.has`, because items do not change during a region sweep.
|
||||
However, `state.can_reach` checks for the very same thing we are updating: Regions.
|
||||
This can lead to non-deterministic behavior and, in the worst case, even generation failures.
|
||||
Even doing `state.can_reach_location` or `state.can_reach_entrance` is problematic, as these functions call `state.can_reach_region` on the respective parent region.
|
||||
|
||||
**Therefore, it is considered unsafe to perform `state.can_reach` from within an access condition for an entrance**, unless you are checking for something that sits in the source region of the entrance.
|
||||
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
||||
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
||||
|
||||
### Item Rules
|
||||
|
||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||
@@ -630,7 +655,7 @@ def set_rules(self) -> None:
|
||||
|
||||
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
|
||||
Entrance should be
|
||||
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
|
||||
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
|
||||
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
|
||||
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
|
||||
For an example, see [The Messenger](/worlds/messenger/rules.py).
|
||||
|
||||
@@ -26,8 +26,17 @@ Unless these are shared between multiple people, we expect the following from ea
|
||||
### Adding a World
|
||||
|
||||
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
|
||||
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
|
||||
in the [CODEOWNERS](/docs/CODEOWNERS) document.
|
||||
nominate someone else (i.e. there are multiple devs).
|
||||
|
||||
### Being added as a maintainer to an existing implementation
|
||||
|
||||
At any point, a world maintainer can approve the addition of another maintainer to their world.
|
||||
In order to do this, either an existing maintainer or the new maintainer must open a PR updating the
|
||||
[CODEOWNERS](/docs/CODEOWNERS) file.
|
||||
This change must be approved by all existing maintainers of the affected world, the new maintainer candidate, and
|
||||
one core maintainer.
|
||||
To help the core team review the change, information about the new maintainer and their contributions should be
|
||||
included in the PR description.
|
||||
|
||||
### Getting Voted
|
||||
|
||||
@@ -35,7 +44,7 @@ When a world is unmaintained, the [core maintainers](https://github.com/orgs/Arc
|
||||
can vote for a new maintainer if there is a candidate.
|
||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||
The time limit is 1 week, but can end early if the majority is reached earlier.
|
||||
Voting shall be conducted on Discord in #archipelago-dev.
|
||||
Voting shall be conducted on Discord in #ap-core-dev.
|
||||
|
||||
## Dropping out
|
||||
|
||||
@@ -51,7 +60,7 @@ for example when they become unreachable.
|
||||
For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
|
||||
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
|
||||
made their case or was pinged and has been unreachable for more than 2 weeks already.
|
||||
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
|
||||
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include
|
||||
date, voting members and final result in the commit message.
|
||||
|
||||
## Handling of Unmaintained Worlds
|
||||
|
||||
@@ -228,8 +228,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a
|
||||
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey;
|
||||
Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "";
|
||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0";
|
||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1""";
|
||||
Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0";
|
||||
Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1""";
|
||||
|
||||
[Code]
|
||||
// See: https://stackoverflow.com/a/51614652/2287576
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=12.0
|
||||
PyYAML>=6.0.1
|
||||
jellyfish>=1.0.3
|
||||
websockets>=13.0.1
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.0
|
||||
jinja2>=3.1.4
|
||||
schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.6.2
|
||||
cython>=3.0.10
|
||||
certifi>=2024.8.30
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.3
|
||||
typing_extensions>=4.12.1
|
||||
orjson>=3.10.7
|
||||
typing_extensions>=4.12.2
|
||||
|
||||
@@ -292,6 +292,14 @@ class World(metaclass=AutoWorldRegister):
|
||||
web: ClassVar[WebWorld] = WebWorld()
|
||||
"""see WebWorld for options"""
|
||||
|
||||
origin_region_name: str = "Menu"
|
||||
"""Name of the Region from which accessibility is tested."""
|
||||
|
||||
explicit_indirect_conditions: bool = True
|
||||
"""If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
|
||||
If False, everything is rechecked at every step, which is slower computationally,
|
||||
but may be desirable in complex/dynamic worlds."""
|
||||
|
||||
multiworld: "MultiWorld"
|
||||
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
||||
player: int
|
||||
|
||||
@@ -26,10 +26,13 @@ class Component:
|
||||
cli: bool
|
||||
func: Optional[Callable]
|
||||
file_identifier: Optional[Callable[[str], bool]]
|
||||
game_name: Optional[str]
|
||||
supports_uri: Optional[bool]
|
||||
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None):
|
||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
||||
self.display_name = display_name
|
||||
self.script_name = script_name
|
||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||
@@ -45,6 +48,8 @@ class Component:
|
||||
Type.ADJUSTER if "Adjuster" in display_name else Type.MISC)
|
||||
self.func = func
|
||||
self.file_identifier = file_identifier
|
||||
self.game_name = game_name
|
||||
self.supports_uri = supports_uri
|
||||
|
||||
def handles_file(self, path: str):
|
||||
return self.file_identifier(path) if self.file_identifier else False
|
||||
@@ -56,10 +61,10 @@ class Component:
|
||||
processes = weakref.WeakSet()
|
||||
|
||||
|
||||
def launch_subprocess(func: Callable, name: str = None):
|
||||
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()):
|
||||
global processes
|
||||
import multiprocessing
|
||||
process = multiprocessing.Process(target=func, name=name)
|
||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||
process.start()
|
||||
processes.add(process)
|
||||
|
||||
@@ -78,9 +83,9 @@ class SuffixIdentifier:
|
||||
return False
|
||||
|
||||
|
||||
def launch_textclient():
|
||||
def launch_textclient(*args):
|
||||
import CommonClient
|
||||
launch_subprocess(CommonClient.run_as_textclient, name="TextClient")
|
||||
launch_subprocess(CommonClient.run_as_textclient, "TextClient", args)
|
||||
|
||||
|
||||
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:
|
||||
|
||||
@@ -101,6 +101,7 @@ class Factorio(World):
|
||||
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
|
||||
tech_mix: int = 0
|
||||
skip_silo: bool = False
|
||||
origin_region_name = "Nauvis"
|
||||
science_locations: typing.List[FactorioScienceLocation]
|
||||
|
||||
settings: typing.ClassVar[FactorioSettings]
|
||||
@@ -125,9 +126,6 @@ class Factorio(World):
|
||||
def create_regions(self):
|
||||
player = self.player
|
||||
random = self.multiworld.random
|
||||
menu = Region("Menu", player, self.multiworld)
|
||||
crash = Entrance(player, "Crash Land", menu)
|
||||
menu.exits.append(crash)
|
||||
nauvis = Region("Nauvis", player, self.multiworld)
|
||||
|
||||
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
|
||||
@@ -184,8 +182,7 @@ class Factorio(World):
|
||||
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
|
||||
location.place_locked_item(event)
|
||||
|
||||
crash.connect(nauvis)
|
||||
self.multiworld.regions += [menu, nauvis]
|
||||
self.multiworld.regions.append(nauvis)
|
||||
|
||||
def create_items(self) -> None:
|
||||
player = self.player
|
||||
|
||||
@@ -601,11 +601,11 @@ class HKWorld(World):
|
||||
if change:
|
||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||
state.prog_items[item.player][effect_name] += effect_value
|
||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
||||
state.prog_items[item.player].get('LEFTDASH', 0):
|
||||
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
||||
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
||||
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||
if state.prog_items[item.player].get('RIGHTDASH', 0) and \
|
||||
state.prog_items[item.player].get('LEFTDASH', 0):
|
||||
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
|
||||
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
|
||||
return change
|
||||
|
||||
def remove(self, state, item: HKItem) -> bool:
|
||||
|
||||
@@ -201,16 +201,13 @@ class KDL3World(World):
|
||||
else:
|
||||
animal_base = ["Rick Spawn", "Kine Spawn", "Coo Spawn", "Nago Spawn", "ChuChu Spawn", "Pitch Spawn"]
|
||||
animal_pool = [self.random.choice(animal_base)
|
||||
for _ in range(len(animal_friend_spawns) - 9)]
|
||||
for _ in range(len(animal_friend_spawns) - 10)]
|
||||
# have to guarantee one of each animal
|
||||
animal_pool.extend(animal_base)
|
||||
if guaranteed_animal == "Kine Spawn":
|
||||
animal_pool.append("Coo Spawn")
|
||||
else:
|
||||
animal_pool.append("Kine Spawn")
|
||||
# Weird fill hack, this forces ChuChu to be the last animal friend placed
|
||||
# If Kine is ever the last animal friend placed, he will cause fill errors on closed world
|
||||
animal_pool.sort()
|
||||
locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
|
||||
items: List[Item] = [self.create_item(animal) for animal in animal_pool]
|
||||
allstate = CollectionState(self.multiworld)
|
||||
|
||||
@@ -19,7 +19,7 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shu
|
||||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
||||
|
||||
components.append(
|
||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game)#, game_name="The Messenger", supports_uri=True)
|
||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
import io
|
||||
import logging
|
||||
import os.path
|
||||
@@ -17,7 +18,7 @@ from Utils import is_windows, messagebox, tuplize_version
|
||||
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
|
||||
|
||||
|
||||
def launch_game(url: Optional[str] = None) -> None:
|
||||
def launch_game(*args) -> None:
|
||||
"""Check the game installation, then launch it"""
|
||||
def courier_installed() -> bool:
|
||||
"""Check if Courier is installed"""
|
||||
@@ -150,15 +151,19 @@ def launch_game(url: Optional[str] = None) -> None:
|
||||
install_mod()
|
||||
elif should_update is None:
|
||||
return
|
||||
|
||||
parser = argparse.ArgumentParser(description="Messenger Client Launcher")
|
||||
parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.")
|
||||
args = parser.parse_args(args)
|
||||
if not is_windows:
|
||||
if url:
|
||||
open_file(f"steam://rungameid/764790//{url}/")
|
||||
if args.url:
|
||||
open_file(f"steam://rungameid/764790//{args.url}/")
|
||||
else:
|
||||
open_file("steam://rungameid/764790")
|
||||
else:
|
||||
os.chdir(game_folder)
|
||||
if url:
|
||||
subprocess.Popen([MessengerWorld.settings.game_path, str(url)])
|
||||
if args.url:
|
||||
subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)])
|
||||
else:
|
||||
subprocess.Popen(MessengerWorld.settings.game_path)
|
||||
os.chdir(working_directory)
|
||||
|
||||
@@ -114,7 +114,6 @@ CONNECTIONS: Dict[str, Dict[str, List[str]]] = {
|
||||
"Forlorn Temple - Rocket Maze Checkpoint",
|
||||
],
|
||||
"Rocket Maze Checkpoint": [
|
||||
"Forlorn Temple - Sunny Day Checkpoint",
|
||||
"Forlorn Temple - Climb Shop",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -122,6 +122,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
game = "Pokemon Emerald"
|
||||
system = "GBA"
|
||||
patch_suffix = ".apemerald"
|
||||
|
||||
local_checked_locations: Set[int]
|
||||
local_set_events: Dict[str, bool]
|
||||
local_found_key_items: Dict[str, bool]
|
||||
@@ -139,8 +140,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
|
||||
current_map: Optional[int]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
def initialize_client(self):
|
||||
self.local_checked_locations = set()
|
||||
self.local_set_events = {}
|
||||
self.local_found_key_items = {}
|
||||
@@ -182,9 +182,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
ctx.want_slot_data = True
|
||||
ctx.watcher_timeout = 0.125
|
||||
|
||||
self.death_counter = None
|
||||
self.previous_death_link = 0
|
||||
self.ignore_next_death_link = False
|
||||
self.initialize_client()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -19,20 +19,20 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
hm_rules: Dict[str, Callable[[CollectionState], bool]] = {}
|
||||
for hm, badges in world.hm_requirements.items():
|
||||
if isinstance(badges, list):
|
||||
hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \
|
||||
and state.has_all(badges, world.player)
|
||||
hm_rules[hm] = lambda state, hm=hm, badges=badges: \
|
||||
state.has(hm, world.player) and state.has_all(badges, world.player)
|
||||
else:
|
||||
hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \
|
||||
and state.has_group("Badges", world.player, badges)
|
||||
hm_rules[hm] = lambda state, hm=hm, badges=badges: \
|
||||
state.has(hm, world.player) and state.has_group_unique("Badges", world.player, badges)
|
||||
|
||||
def has_acro_bike(state: CollectionState):
|
||||
return state.has("Acro Bike", world.player)
|
||||
|
||||
def has_mach_bike(state: CollectionState):
|
||||
return state.has("Mach Bike", world.player)
|
||||
|
||||
|
||||
def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool:
|
||||
return sum([state.has(event, world.player) for event in [
|
||||
return state.has_from_list_unique([
|
||||
"EVENT_DEFEAT_ROXANNE",
|
||||
"EVENT_DEFEAT_BRAWLY",
|
||||
"EVENT_DEFEAT_WATTSON",
|
||||
@@ -41,7 +41,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
"EVENT_DEFEAT_WINONA",
|
||||
"EVENT_DEFEAT_TATE_AND_LIZA",
|
||||
"EVENT_DEFEAT_JUAN",
|
||||
]]) >= n
|
||||
], world.player, n)
|
||||
|
||||
huntable_legendary_events = [
|
||||
f"EVENT_ENCOUNTER_{key}"
|
||||
@@ -61,8 +61,9 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
}.items()
|
||||
if name in world.options.allowed_legendary_hunt_encounters.value
|
||||
]
|
||||
|
||||
def encountered_n_legendaries(state: CollectionState, n: int) -> bool:
|
||||
return sum(int(state.has(event, world.player)) for event in huntable_legendary_events) >= n
|
||||
return state.has_from_list_unique(huntable_legendary_events, world.player, n)
|
||||
|
||||
def get_entrance(entrance: str):
|
||||
return world.multiworld.get_entrance(entrance, world.player)
|
||||
@@ -235,11 +236,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
if world.options.norman_requirement == NormanRequirement.option_badges:
|
||||
set_rule(
|
||||
get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"),
|
||||
lambda state: state.has_group("Badges", world.player, world.options.norman_count.value)
|
||||
lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"),
|
||||
lambda state: state.has_group("Badges", world.player, world.options.norman_count.value)
|
||||
lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value)
|
||||
)
|
||||
else:
|
||||
set_rule(
|
||||
@@ -299,15 +300,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_116_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_116_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_116_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_116_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
|
||||
# Rusturf Tunnel
|
||||
@@ -347,19 +348,19 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"),
|
||||
lambda state: has_mach_bike(state)
|
||||
has_mach_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_115_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_115_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_115_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_115_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
|
||||
if world.options.extra_boulders:
|
||||
@@ -375,7 +376,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
if world.options.extra_bumpy_slope:
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"),
|
||||
lambda state: has_acro_bike(state)
|
||||
has_acro_bike
|
||||
)
|
||||
else:
|
||||
set_rule(
|
||||
@@ -386,17 +387,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
# Route 105
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_105_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_105_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_105_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_105_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"),
|
||||
@@ -439,7 +440,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"),
|
||||
lambda state: has_mach_bike(state)
|
||||
has_mach_bike
|
||||
)
|
||||
|
||||
# Route 107
|
||||
@@ -643,15 +644,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_114_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_114_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_114_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_114_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
|
||||
# Meteor Falls
|
||||
@@ -699,11 +700,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
# Jagged Pass
|
||||
set_rule(
|
||||
get_entrance("REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"),
|
||||
lambda state: has_acro_bike(state)
|
||||
has_acro_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"),
|
||||
lambda state: has_acro_bike(state)
|
||||
has_acro_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"),
|
||||
@@ -719,11 +720,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
# Mirage Tower
|
||||
set_rule(
|
||||
get_entrance("REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"),
|
||||
lambda state: has_mach_bike(state)
|
||||
has_mach_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"),
|
||||
lambda state: has_mach_bike(state)
|
||||
has_mach_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"),
|
||||
@@ -812,15 +813,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_118_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_118_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"),
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("TERRA_CAVE_ROUTE_118_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("TERRA_CAVE_ROUTE_118_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
|
||||
# Route 119
|
||||
@@ -830,11 +831,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"),
|
||||
lambda state: has_acro_bike(state)
|
||||
has_acro_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"),
|
||||
lambda state: has_acro_bike(state)
|
||||
has_acro_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"),
|
||||
@@ -850,7 +851,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"),
|
||||
lambda state: has_acro_bike(state)
|
||||
has_acro_bike
|
||||
)
|
||||
if "Route 119 Aqua Grunts" not in world.options.remove_roadblocks.value:
|
||||
set_rule(
|
||||
@@ -927,11 +928,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"),
|
||||
lambda state: has_acro_bike(state)
|
||||
has_acro_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"),
|
||||
lambda state: has_mach_bike(state)
|
||||
has_mach_bike
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"),
|
||||
@@ -1115,17 +1116,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
# Route 125
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_125_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_125_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_125_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_125_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
|
||||
# Shoal Cave
|
||||
@@ -1257,17 +1258,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_127_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_127_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_127_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_127_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
|
||||
# Route 128
|
||||
@@ -1374,17 +1375,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
# Route 129
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_129_1", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_129_1", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"),
|
||||
lambda state: hm_rules["HM08 Dive"](state) and \
|
||||
state.has("EVENT_DEFEAT_CHAMPION", world.player) and \
|
||||
state.has("MARINE_CAVE_ROUTE_129_2", world.player) and \
|
||||
state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
lambda state: hm_rules["HM08 Dive"](state)
|
||||
and state.has("EVENT_DEFEAT_CHAMPION", world.player)
|
||||
and state.has("MARINE_CAVE_ROUTE_129_2", world.player)
|
||||
and state.has("EVENT_DEFEAT_SHELLY", world.player)
|
||||
)
|
||||
|
||||
# Pacifidlog Town
|
||||
@@ -1505,7 +1506,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
if world.options.elite_four_requirement == EliteFourRequirement.option_badges:
|
||||
set_rule(
|
||||
get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"),
|
||||
lambda state: state.has_group("Badges", world.player, world.options.elite_four_count.value)
|
||||
lambda state: state.has_group_unique("Badges", world.player, world.options.elite_four_count.value)
|
||||
)
|
||||
else:
|
||||
set_rule(
|
||||
|
||||
@@ -49,6 +49,30 @@ class RLWorld(World):
|
||||
return {option_name: self.get_setting(option_name).value for option_name in rl_options}
|
||||
|
||||
def generate_early(self):
|
||||
location_ids_used_per_game = {
|
||||
world.game: set(world.location_id_to_name) for world in self.multiworld.worlds.values()
|
||||
}
|
||||
item_ids_used_per_game = {
|
||||
world.game: set(world.item_id_to_name) for world in self.multiworld.worlds.values()
|
||||
}
|
||||
overlapping_games = set()
|
||||
|
||||
for id_lookup in (location_ids_used_per_game, item_ids_used_per_game):
|
||||
for game_1, ids_1 in id_lookup.items():
|
||||
for game_2, ids_2 in id_lookup.items():
|
||||
if game_1 == game_2:
|
||||
continue
|
||||
|
||||
if ids_1 & ids_2:
|
||||
overlapping_games.add(tuple(sorted([game_1, game_2])))
|
||||
|
||||
if overlapping_games:
|
||||
raise RuntimeError(
|
||||
"In this multiworld, there are games with overlapping item/location IDs.\n"
|
||||
"The current Rogue Legacy does not support these and a fix is not currently planned.\n"
|
||||
f"The overlapping games are: {overlapping_games}"
|
||||
)
|
||||
|
||||
# Check validation of names.
|
||||
additional_lady_names = len(self.get_setting("additional_lady_names").value)
|
||||
additional_sir_names = len(self.get_setting("additional_sir_names").value)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Starcraft 2
|
||||
# StarCraft 2
|
||||
|
||||
## Game page in other languages:
|
||||
* [Français](/games/Starcraft%202/info/fr)
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
The following unlocks are randomized as items:
|
||||
1. Your ability to build any non-worker unit.
|
||||
2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss!
|
||||
2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain
|
||||
choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss!
|
||||
3. Your ability to get the generic unit upgrades, such as attack and armour upgrades.
|
||||
4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades for Zerg, and Spear of Adun upgrades for Protoss.
|
||||
4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades
|
||||
for Zerg, and Spear of Adun upgrades for Protoss.
|
||||
5. Small boosts to your starting mineral, vespene gas, and supply totals on each mission.
|
||||
|
||||
You find items by making progress in these categories:
|
||||
@@ -18,50 +20,91 @@ You find items by making progress in these categories:
|
||||
* Reaching milestones in the mission, such as completing part of a main objective
|
||||
* Completing challenges based on achievements in the base game, such as clearing all Zerg on Devil's Playground
|
||||
|
||||
Except for mission completion, these categories can be disabled in the game's settings. For instance, you can disable getting items for reaching required milestones.
|
||||
In Archipelago's nomenclature, these are the locations where items can be found.
|
||||
Each location, including mission completion, has a set of rules that specify the items required to access it.
|
||||
These rules were designed assuming that StarCraft 2 is played on the Brutal difficulty.
|
||||
Since each location has its own rule, it's possible that an item required for progression is in a mission where you
|
||||
can't reach all of its locations or complete it.
|
||||
However, mission completion is always required to gain access to new missions.
|
||||
|
||||
Aside from mission completion, the other location categories can be disabled in the player options.
|
||||
For instance, you can disable getting items for reaching required milestones.
|
||||
|
||||
When you receive items, they will immediately become available, even during a mission, and you will be
|
||||
notified via a text box in the top-right corner of the game screen. Item unlocks are also logged in the Archipelago client.
|
||||
notified via a text box in the top-right corner of the game screen.
|
||||
Item unlocks are also logged in the Archipelago client.
|
||||
|
||||
Missions are launched through the Starcraft 2 Archipelago client, through the Starcraft 2 Launcher tab. The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. Additionally, metaprogression currencies such as credits and Solarite are not used.
|
||||
Missions are launched through the StarCraft 2 Archipelago client, through the StarCraft 2 Launcher tab.
|
||||
The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included.
|
||||
Additionally, metaprogression currencies such as credits and Solarite are not used.
|
||||
|
||||
## What is the goal of this game when randomized?
|
||||
|
||||
The goal is to beat the final mission in the mission order. The yaml configuration file controls the mission order and how missions are shuffled.
|
||||
The goal is to beat the final mission in the mission order.
|
||||
The yaml configuration file controls the mission order (e.g. blitz, grid, etc.), which combination of the four
|
||||
StarCraft 2 campaigns can be used to populate the mission order and how missions are shuffled.
|
||||
Since the first two options determine the number of missions in a StarCraft 2 world, they can be used to customize the
|
||||
expected time to complete the world.
|
||||
Note that the evolution missions from Heart of the Swarm are not included in the randomizer.
|
||||
|
||||
## What non-randomized changes are there from vanilla Starcraft 2?
|
||||
## What non-randomized changes are there from vanilla StarCraft 2?
|
||||
|
||||
1. Some missions have more vespene geysers available to allow a wider variety of units.
|
||||
2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, brood war, and original ideas.
|
||||
3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer have tech requirements.
|
||||
2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions,
|
||||
brood war, and original ideas.
|
||||
3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer
|
||||
have tech requirements.
|
||||
4. Zerg missions have been adjusted to give the player a starting Lair where they would only have Hatcheries.
|
||||
5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors taking longer to build.
|
||||
6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them without getting stuck in odd places.
|
||||
5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors
|
||||
taking longer to build.
|
||||
6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them
|
||||
without getting stuck in odd places.
|
||||
7. Several vanilla bugs have been fixed.
|
||||
|
||||
## Which of my items can be in another player's world?
|
||||
|
||||
By default, any of StarCraft 2's items (specified above) can be in another player's world. See the
|
||||
[Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en)
|
||||
for more information on how to change this.
|
||||
By default, any of StarCraft 2's items (specified above) can be in another player's world.
|
||||
See the [Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) for more information on how to change this.
|
||||
|
||||
## Unique Local Commands
|
||||
|
||||
The following commands are only available when using the Starcraft 2 Client to play with Archipelago. You can list them any time in the client with `/help`.
|
||||
The following commands are only available when using the StarCraft 2 Client to play with Archipelago.
|
||||
You can list them any time in the client with `/help`.
|
||||
|
||||
* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files
|
||||
* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago.
|
||||
Will overwrite existing files
|
||||
* `/difficulty [difficulty]` Overrides the difficulty set for the world.
|
||||
* Options: casual, normal, hard, brutal
|
||||
* `/game_speed [game_speed]` Overrides the game speed for the world
|
||||
* Options: default, slower, slow, normal, fast, faster
|
||||
* `/color [faction] [color]` Changes your color for one of your playable factions.
|
||||
* Faction options: raynor, kerrigan, primal, protoss, nova
|
||||
* Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, lightgreen, darkgrey, pink, rainbow, random, default
|
||||
* Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen,
|
||||
brown, lightgreen, darkgrey, pink, rainbow, random, default
|
||||
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
|
||||
* Run without arguments to list all options.
|
||||
* Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource amounts, controlling AI allies, etc.
|
||||
* `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing.
|
||||
* `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided
|
||||
* Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource
|
||||
amounts, controlling AI allies, etc.
|
||||
* `/disable_mission_check` Disables the check to see if a mission is available to play.
|
||||
Meant for co-op runs where one player can play the next mission in a chain the other player is doing.
|
||||
* `/play [mission_id]` Starts a StarCraft 2 mission based off of the mission_id provided
|
||||
* `/available` Get what missions are currently available to play
|
||||
* `/unfinished` Get what missions are currently available to play and have not had all locations checked
|
||||
* `/set_path [path]` Manually set the SC2 install directory (if the automatic detection fails)
|
||||
|
||||
Note that the behavior of the command `/received` was modified in the StarCraft 2 client.
|
||||
In the Common client of Archipelago, the command returns the list of items received in the reverse order they were
|
||||
received.
|
||||
In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg).
|
||||
Additionally, upgrades are grouped beneath their corresponding units or buildings.
|
||||
A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown.
|
||||
Every item whose name, race, or group name contains the provided parameter will be shown.
|
||||
|
||||
## Known issues
|
||||
|
||||
- StarCraft 2 Archipelago does not support loading a saved game.
|
||||
For this reason, it is recommended to play on a difficulty level lower than what you are normally comfortable with.
|
||||
- StarCraft 2 Archipelago does not support the restart of a mission from the StarCraft 2 menu.
|
||||
To restart a mission, use the StarCraft 2 Client.
|
||||
- A crash report is often generated when a mission is closed.
|
||||
This does not affect the game and can be ignored.
|
||||
|
||||
@@ -21,6 +21,14 @@ Les *items* sont trouvés en accomplissant du progrès dans les catégories suiv
|
||||
* Réussir des défis basés sur les succès du jeu de base, e.g. éliminer tous les *Zerg* dans la mission
|
||||
*Devil's Playground*
|
||||
|
||||
Dans la nomenclature d'Archipelago, il s'agit des *locations* où l'on peut trouver des *items*.
|
||||
Pour chaque *location*, incluant le fait de terminer une mission, il y a des règles qui définissent les *items*
|
||||
nécessaires pour y accéder.
|
||||
Ces règles ont été conçues en assumant que *StarCraft 2* est joué à la difficulté *Brutal*.
|
||||
Étant donné que chaque *location* a ses propres règles, il est possible qu'un *item* nécessaire à la progression se
|
||||
trouve dans une mission dont vous ne pouvez pas atteindre toutes les *locations* ou que vous ne pouvez pas terminer.
|
||||
Cependant, il est toujours nécessaire de terminer une mission pour pouvoir accéder à de nouvelles missions.
|
||||
|
||||
Ces catégories, outre la première, peuvent être désactivées dans les options du jeu.
|
||||
Par exemple, vous pouvez désactiver le fait d'obtenir des *items* lorsque des étapes importantes d'une mission sont
|
||||
accomplies.
|
||||
@@ -37,8 +45,13 @@ Archipelago*.
|
||||
|
||||
## Quel est le but de ce jeu quand il est *randomized*?
|
||||
|
||||
Le but est de réussir la mission finale dans la disposition des missions (e.g. *blitz*, *grid*, etc.).
|
||||
Les choix faits dans le fichier *yaml* définissent la disposition des missions et comment elles sont mélangées.
|
||||
Le but est de réussir la mission finale du *mission order* (e.g. *blitz*, *grid*, etc.).
|
||||
Le fichier de configuration yaml permet de spécifier le *mission order*, lesquelles des quatre campagnes de
|
||||
*StarCraft 2* peuvent être utilisées pour remplir le *mission order* et comment les missions sont distribuées dans le
|
||||
*mission order*.
|
||||
Étant donné que les deux premières options déterminent le nombre de missions dans un monde de *StarCraft 2*, elles
|
||||
peuvent être utilisées pour moduler le temps nécessaire pour terminer le monde.
|
||||
Notez que les missions d'évolution de Heart of the Swarm ne sont pas incluses dans le *randomizer*.
|
||||
|
||||
## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2*
|
||||
|
||||
@@ -93,3 +106,20 @@ mission de la chaîne qu'un autre joueur est en train d'entamer.
|
||||
l'accès à un *item* n'ont pas été accomplis.
|
||||
* `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la
|
||||
détection automatique de cette dernière échoue.
|
||||
|
||||
Notez que le comportement de la commande `/received` a été modifié dans le client *StarCraft 2*.
|
||||
Dans le client *Common* d'Archipelago, elle renvoie la liste des *items* reçus dans l'ordre inverse de leur réception.
|
||||
Dans le client de *StarCraft 2*, la liste est divisée par races (i.e., *Any*, *Protoss*, *Terran*, et *Zerg*).
|
||||
De plus, les améliorations sont regroupées sous leurs unités/bâtiments correspondants.
|
||||
Un paramètre de filtrage peut aussi être fourni, e.g., `/received Thor`, pour limiter le nombre d'*items* affichés.
|
||||
Tous les *items* dont le nom, la race ou le nom de groupe contient le paramètre fourni seront affichés.
|
||||
|
||||
## Problèmes connus
|
||||
|
||||
- *StarCraft 2 Archipelago* ne supporte pas le chargement d'une sauvegarde.
|
||||
Pour cette raison, il est recommandé de jouer à un niveau de difficulté inférieur à celui avec lequel vous êtes
|
||||
normalement à l'aise.
|
||||
- *StarCraft 2 Archipelago* ne supporte pas le redémarrage d'une mission depuis le menu de *StarCraft 2*.
|
||||
Pour redémarrer une mission, utilisez le client de *StarCraft 2 Archipelago*.
|
||||
- Un rapport d'erreur est souvent généré lorsqu'une mission est fermée.
|
||||
Cela n'affecte pas le jeu et peut être ignoré.
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
# StarCraft 2 Randomizer Setup Guide
|
||||
|
||||
This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where
|
||||
to obtain a config file for StarCraft 2.
|
||||
This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as
|
||||
where to obtain a config file for StarCraft 2.
|
||||
|
||||
## Required Software
|
||||
|
||||
- [StarCraft 2](https://starcraft2.com/en-us/)
|
||||
- While StarCraft 2 Archipelago supports all four campaigns, they are not mandatory to play the randomizer.
|
||||
If you do not own certain campaigns, you only need to exclude them in the configuration file of your world.
|
||||
- [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
## How do I install this randomizer?
|
||||
|
||||
1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the Archipelago installer.
|
||||
1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the
|
||||
Archipelago installer.
|
||||
- Linux users should also follow the instructions found at the bottom of this page
|
||||
(["Running in Linux"](#running-in-linux)).
|
||||
2. Run ArchipelagoStarcraft2Client.exe.
|
||||
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
|
||||
3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above.
|
||||
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step
|
||||
only.
|
||||
3. Type the command `/download_data`.
|
||||
This will automatically install the Maps and Data files needed to play StarCraft 2 Archipelago.
|
||||
|
||||
## Where do I get a config file (aka "YAML") for this game?
|
||||
|
||||
Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only using default options.
|
||||
Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only
|
||||
using default options.
|
||||
When you're setting up a multiworld, every world needs its own yaml file.
|
||||
|
||||
There are three basic ways to get a yaml:
|
||||
* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml.
|
||||
* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice.
|
||||
* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export
|
||||
the yaml.
|
||||
* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options)
|
||||
page or by generating it from the Launcher (`ArchipelagoLauncher.exe`).
|
||||
The template includes descriptions of each option, you just have to edit it in your text editor of choice.
|
||||
* You can ask someone else to share their yaml to use it for yourself or adjust it as you wish.
|
||||
|
||||
Remember the name you enter in the options page or in the yaml file, you'll need it to connect later!
|
||||
@@ -36,15 +45,31 @@ Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for
|
||||
|
||||
The simplest way to check is to use the website [validator](/check).
|
||||
|
||||
You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder.
|
||||
You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the `Players/` folder
|
||||
within your Archipelago installation and run `ArchipelagoGenerate.exe`.
|
||||
You should see a new `.zip` file within the `output/` folder of your Archipelago installation if things worked
|
||||
correctly.
|
||||
It's advisable to run `ArchipelagoGenerate.exe` through a terminal so that you can see the printout, which will include
|
||||
any errors and the precise output file name if it's successful.
|
||||
If you don't like terminals, you can also check the log file in the `logs/` folder.
|
||||
|
||||
#### What does Progression Balancing do?
|
||||
|
||||
For Starcraft 2, not much. It's an Archipelago-wide option meant to shift required items earlier in the playthrough, but Starcraft 2 tends to be much more open in what items you can use. As such, this adjustment isn't very noticeable. It can also increase generation times, so we generally recommend turning it off.
|
||||
For StarCraft 2, this option doesn't have much impact.
|
||||
It is an Archipelago option designed to balance world progression by swapping items in spheres.
|
||||
If the Progression Balancing of one world is greater than that of others, items in that world are more likely to be
|
||||
obtained early, and vice versa if its value is smaller.
|
||||
However, StarCraft 2 is more permissive regarding the items that can be used to progress, so this option has little
|
||||
influence on progression in a StarCraft 2 world.
|
||||
StarCraft 2.
|
||||
Since this option increases the time required to generate a MultiWorld, we recommend deactivating it (i.e., setting it
|
||||
to zero) for a StarCraft 2 world.
|
||||
|
||||
#### How do I specify items in a list, like in excluded items?
|
||||
|
||||
You can look up the syntax for yaml collections in the [YAML specification](https://yaml.org/spec/1.2.2/#21-collections). For lists, every item goes on its own line, started with a hyphen:
|
||||
You can look up the syntax for yaml collections in the
|
||||
[YAML specification](https://yaml.org/spec/1.2.2/#21-collections).
|
||||
For lists, every item goes on its own line, started with a hyphen:
|
||||
|
||||
```yaml
|
||||
excluded_items:
|
||||
@@ -52,11 +77,13 @@ excluded_items:
|
||||
- Drop-Pods (Kerrigan Tier 7)
|
||||
```
|
||||
|
||||
An empty list is just a matching pair of square brackets: `[]`. That's the default value in the template, which should let you know to use this syntax.
|
||||
An empty list is just a matching pair of square brackets: `[]`.
|
||||
That's the default value in the template, which should let you know to use this syntax.
|
||||
|
||||
#### How do I specify items for the starting inventory?
|
||||
|
||||
The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value:
|
||||
The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with.
|
||||
The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value:
|
||||
|
||||
```yaml
|
||||
start_inventory:
|
||||
@@ -64,37 +91,61 @@ start_inventory:
|
||||
Additional Starting Vespene: 5
|
||||
```
|
||||
|
||||
An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax.
|
||||
An empty mapping is just a matching pair of curly braces: `{}`.
|
||||
That's the default value in the template, which should let you know to use this syntax.
|
||||
|
||||
#### How do I know the exact names of items and locations?
|
||||
|
||||
The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations for each game that it currently supports, including StarCraft 2.
|
||||
The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations
|
||||
for each game that it currently supports, including StarCraft 2.
|
||||
|
||||
You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page.
|
||||
You can also look up a complete list of the item names in the
|
||||
[Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page.
|
||||
This page also contains supplementary information of each item.
|
||||
However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development.
|
||||
However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the
|
||||
former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development.
|
||||
|
||||
As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client.
|
||||
As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over
|
||||
the mission in the 'StarCraft 2 Launcher' tab in the client.
|
||||
|
||||
## How do I join a MultiWorld game?
|
||||
|
||||
1. Run ArchipelagoStarcraft2Client.exe.
|
||||
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only.
|
||||
- macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step
|
||||
only.
|
||||
2. Type `/connect [server ip]`.
|
||||
- If you're running through the website, the server IP should be displayed near the top of the room page.
|
||||
3. Type your slot name from your YAML when prompted.
|
||||
4. If the server has a password, enter that when prompted.
|
||||
5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your world. Unreachable missions will have greyed-out text. Just click on an available mission to start it!
|
||||
5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your
|
||||
world.
|
||||
Unreachable missions will have greyed-out text. Just click on an available mission to start it!
|
||||
|
||||
## The game isn't launching when I try to start a mission.
|
||||
|
||||
First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out
|
||||
the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a
|
||||
specific description of what's going wrong and attach your log file to your message.
|
||||
First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`).
|
||||
If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel
|
||||
for help.
|
||||
Please include a specific description of what's going wrong and attach your log file to your message.
|
||||
|
||||
## My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*.
|
||||
|
||||
For your keyboard shortcuts profile to work in Archipelago, you need to copy your shortcuts file from
|
||||
`Documents/StarCraft II/Accounts/######/Hotkeys` to `Documents/StarCraft II/Hotkeys`.
|
||||
If the folder doesn't exist, create it.
|
||||
|
||||
To enable StarCraft 2 Archipelago to use your profile, follow these steps:
|
||||
1. Launch StarCraft 2 via the Battle.net application.
|
||||
2. Change your hotkey profile to the standard mode and accept.
|
||||
3. Select your custom profile and accept.
|
||||
|
||||
You will only need to do this once.
|
||||
|
||||
## Running in macOS
|
||||
|
||||
To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](/tutorial/Archipelago/mac/en). Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`.
|
||||
To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here:
|
||||
[macOS Guide](/tutorial/Archipelago/mac/en).
|
||||
Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`.
|
||||
|
||||
## Running in Linux
|
||||
|
||||
@@ -102,9 +153,9 @@ To run StarCraft 2 through Archipelago in Linux, you will need to install the ga
|
||||
of the Archipelago client.
|
||||
|
||||
Make sure you have StarCraft 2 installed using Wine, and that you have followed the
|
||||
[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not
|
||||
need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the
|
||||
Lutris installer.
|
||||
[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location.
|
||||
You will not need to copy the `.dll` files.
|
||||
If you're having trouble installing or running StarCraft 2 on Linux, it is recommend to use the Lutris installer.
|
||||
|
||||
Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant
|
||||
locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same
|
||||
@@ -139,5 +190,5 @@ below, replacing **${ID}** with the numerical ID.
|
||||
lutris lutris:rungameid/${ID} --output-script sc2.sh
|
||||
|
||||
This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path
|
||||
to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code
|
||||
above into the existing script.
|
||||
to the Wine binary that Lutris uses.
|
||||
You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script.
|
||||
|
||||
@@ -6,6 +6,10 @@ indications pour obtenir un fichier de configuration de *StarCraft 2 Archipelago
|
||||
## Logiciels requis
|
||||
|
||||
- [*StarCraft 2*](https://starcraft2.com/en-us/)
|
||||
- Bien que *StarCraft 2 Archipelago* supporte les quatre campagnes, elles ne sont pas obligatoires pour jouer au
|
||||
*randomizer*.
|
||||
Si vous ne possédez pas certaines campagnes, il vous suffit de les exclure dans le fichier de configuration de
|
||||
votre monde.
|
||||
- [La version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
## Comment est-ce que j'installe ce *randomizer*?
|
||||
@@ -41,10 +45,6 @@ préférences.
|
||||
Prenez soin de vous rappeler du nom de joueur que vous avez inscrit dans la page à options ou dans le fichier *yaml*
|
||||
puisque vous en aurez besoin pour vous connecter à votre monde!
|
||||
|
||||
Notez que la page *Player options* ne permet pas de définir certaines des options avancées, e.g., l'exclusion de
|
||||
certaines unités ou de leurs améliorations.
|
||||
Utilisez la page [*Weighted Options*](/weighted-options) pour avoir accès à ces dernières.
|
||||
|
||||
Si vous désirez des informations et/ou instructions générales sur l'utilisation d'un fichier *yaml* pour Archipelago,
|
||||
veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml).
|
||||
|
||||
@@ -66,15 +66,15 @@ dans le dossier `logs/`.
|
||||
|
||||
#### À quoi sert l'option *Progression Balancing*?
|
||||
|
||||
Pour *Starcraft 2*, cette option ne fait pas grand-chose.
|
||||
Pour *StarCraft 2*, cette option ne fait pas grand-chose.
|
||||
Il s'agit d'une option d'Archipelago permettant d'équilibrer la progression des mondes en interchangeant les *items*
|
||||
dans les *spheres*.
|
||||
Si le *Progression Balancing* d'un monde est plus grand que ceux des autres, les *items* de progression de ce monde ont
|
||||
plus de chance d'être obtenus tôt et vice-versa si sa valeur est plus petite que celle des autres mondes.
|
||||
Cependant, *Starcraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à
|
||||
Cependant, *StarCraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à
|
||||
donc peu d'influence sur la progression dans *StarCraft 2*.
|
||||
Vu qu'il augmente le temps de génération d'un *MultiWorld*, nous recommandons de le désactiver, c-à-d le définir à
|
||||
zéro, pour *Starcraft 2*.
|
||||
zéro, pour *StarCraft 2*.
|
||||
|
||||
|
||||
#### Comment est-ce que je définis une liste d'*items*, e.g. pour l'option *excluded items*?
|
||||
@@ -122,6 +122,10 @@ Cependant, l'information présente dans cette dernière peut différer de celle
|
||||
puisqu'elle est générée, habituellement, à partir de la version en développement de *StarCraft 2 Archipelago* qui
|
||||
n'ont peut-être pas encore été inclus dans le site web d'Archipelago.
|
||||
|
||||
Pour ce qui concerne les *locations*, vous pouvez consulter tous les *locations* associés à une mission dans votre
|
||||
monde en plaçant votre curseur sur la case correspondante dans l'onglet *StarCraft 2 Launcher* du client.
|
||||
|
||||
|
||||
## Comment est-ce que je peux joindre un *MultiWorld*?
|
||||
|
||||
1. Exécuter `ArchipelagoStarcraft2Client.exe`.
|
||||
@@ -152,7 +156,7 @@ qui se trouve dans `Documents/StarCraft II/Accounts/######/Hotkeys` vers `Docume
|
||||
Si le dossier n'existe pas, créez-le.
|
||||
|
||||
Pour que *StarCraft 2 Archipelago* utilise votre profil, suivez les étapes suivantes.
|
||||
Lancez *Starcraft 2* via l'application *Battle.net*.
|
||||
Lancez *StarCraft 2* via l'application *Battle.net*.
|
||||
Changez votre profil de raccourcis clavier pour le mode standard et acceptez, puis sélectionnez votre profil
|
||||
personnalisé et acceptez.
|
||||
Vous n'aurez besoin de faire ça qu'une seule fois.
|
||||
|
||||
@@ -45,7 +45,7 @@ class SubnauticaWorld(World):
|
||||
options_dataclass = options.SubnauticaOptions
|
||||
options: options.SubnauticaOptions
|
||||
required_client_version = (0, 5, 0)
|
||||
|
||||
origin_region_name = "Planet 4546B"
|
||||
creatures_to_scan: List[str]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
@@ -66,13 +66,9 @@ class SubnauticaWorld(World):
|
||||
creature_pool, self.options.creature_scans.value)
|
||||
|
||||
def create_regions(self):
|
||||
# Create Regions
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
# Create Region
|
||||
planet_region = Region("Planet 4546B", self.player, self.multiworld)
|
||||
|
||||
# Link regions together
|
||||
menu_region.connect(planet_region, "Lifepod 5")
|
||||
|
||||
# Create regular locations
|
||||
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
|
||||
(creature + creatures.suffix for creature in self.creatures_to_scan))
|
||||
@@ -93,11 +89,8 @@ class SubnauticaWorld(World):
|
||||
# make the goal event the victory "item"
|
||||
location.item.name = "Victory"
|
||||
|
||||
# Register regions to multiworld
|
||||
self.multiworld.regions += [
|
||||
menu_region,
|
||||
planet_region
|
||||
]
|
||||
# Register region to multiworld
|
||||
self.multiworld.regions.append(planet_region)
|
||||
|
||||
# refer to rules.py
|
||||
set_rules = set_rules
|
||||
|
||||
@@ -204,11 +204,10 @@ class WitnessWorld(World):
|
||||
]
|
||||
if early_items:
|
||||
random_early_item = self.random.choice(early_items)
|
||||
if (
|
||||
self.options.puzzle_randomization == "sigma_expert"
|
||||
or self.options.victory_condition == "panel_hunt"
|
||||
):
|
||||
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
|
||||
mode = self.options.puzzle_randomization
|
||||
if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt":
|
||||
# In Expert and Variety, only tag the item as early, rather than forcing it onto the gate.
|
||||
# Same with panel hunt, since the Tutorial Gate Open panel is used for something else
|
||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||
else:
|
||||
# Force the item onto the tutorial gate check and remove it from our random pool.
|
||||
@@ -255,7 +254,7 @@ class WitnessWorld(World):
|
||||
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
|
||||
|
||||
warning(
|
||||
f"""Location "{loc}" had to be added to {self.player_name}'s world
|
||||
f"""Location "{loc}" had to be added to {self.player_name}'s world
|
||||
due to insufficient sphere 1 size."""
|
||||
)
|
||||
|
||||
|
||||
1223
worlds/witness/data/WitnessLogicVariety.txt
Normal file
1223
worlds/witness/data/WitnessLogicVariety.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,22 @@ ITEM_GROUPS: Dict[str, Set[str]] = {}
|
||||
# item list during get_progression_items.
|
||||
_special_usefuls: List[str] = ["Puzzle Skip"]
|
||||
|
||||
ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
||||
|
||||
MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = {
|
||||
"none": set(),
|
||||
"sigma_normal": set(),
|
||||
"sigma_expert": {"Triangles"},
|
||||
"umbra_variety": {"Triangles"}
|
||||
}
|
||||
|
||||
MODE_SPECIFIC_GOOD_DISCARD_ITEMS: Dict[str, Set[str]] = {
|
||||
"none": {"Triangles"},
|
||||
"sigma_normal": {"Triangles"},
|
||||
"sigma_expert": {"Arrows"},
|
||||
"umbra_variety": set() # Variety Discards use both Arrows and Triangles, so neither of them are that useful alone
|
||||
}
|
||||
|
||||
|
||||
def populate_items() -> None:
|
||||
for item_name, definition in static_witness_logic.ALL_ITEMS.items():
|
||||
|
||||
@@ -17,6 +17,7 @@ from .utils import (
|
||||
get_items,
|
||||
get_sigma_expert_logic,
|
||||
get_sigma_normal_logic,
|
||||
get_umbra_variety_logic,
|
||||
get_vanilla_logic,
|
||||
logical_or_witness_rules,
|
||||
parse_lambda,
|
||||
@@ -292,6 +293,11 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
|
||||
return StaticWitnessLogicObj(get_sigma_expert_logic())
|
||||
|
||||
|
||||
@cache_argsless
|
||||
def get_umbra_variety() -> StaticWitnessLogicObj:
|
||||
return StaticWitnessLogicObj(get_umbra_variety_logic())
|
||||
|
||||
|
||||
def __getattr__(name: str) -> StaticWitnessLogicObj:
|
||||
if name == "vanilla":
|
||||
return get_vanilla()
|
||||
@@ -299,6 +305,8 @@ def __getattr__(name: str) -> StaticWitnessLogicObj:
|
||||
return get_sigma_normal()
|
||||
if name == "sigma_expert":
|
||||
return get_sigma_expert()
|
||||
if name == "umbra_variety":
|
||||
return get_umbra_variety()
|
||||
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||
|
||||
|
||||
|
||||
@@ -215,6 +215,10 @@ def get_sigma_expert_logic() -> List[str]:
|
||||
return get_adjustment_file("WitnessLogicExpert.txt")
|
||||
|
||||
|
||||
def get_umbra_variety_logic() -> List[str]:
|
||||
return get_adjustment_file("WitnessLogicVariety.txt")
|
||||
|
||||
|
||||
def get_vanilla_logic() -> List[str]:
|
||||
return get_adjustment_file("WitnessLogicVanilla.txt")
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ class EntityHuntPicker:
|
||||
|
||||
remaining_entities, remaining_entity_weights = [], []
|
||||
for area, eligible_entities in self.ELIGIBLE_ENTITIES_PER_AREA.items():
|
||||
for panel in eligible_entities - self.HUNT_ENTITIES:
|
||||
for panel in sorted(eligible_entities - self.HUNT_ENTITIES):
|
||||
remaining_entities.append(panel)
|
||||
remaining_entity_weights.append(allowance_per_area[area])
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
|
||||
wincon = world.options.victory_condition
|
||||
|
||||
if discards:
|
||||
if difficulty == "sigma_expert":
|
||||
if difficulty == "sigma_expert" or difficulty == "umbra_variety":
|
||||
always.append("Arrows")
|
||||
else:
|
||||
always.append("Triangles")
|
||||
|
||||
@@ -250,10 +250,15 @@ class PanelHuntDiscourageSameAreaFactor(Range):
|
||||
class PuzzleRandomization(Choice):
|
||||
"""
|
||||
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
|
||||
"Sigma Normal" randomizes puzzles close to their original mechanics and difficulty.
|
||||
"Sigma Expert" is an entirely new experience with extremely difficult random puzzles. Do not underestimate this mode, it is brutal.
|
||||
"Umbra Variety" focuses on unique symbol combinations not featured in the original game. It is harder than Sigma Normal, but easier than Sigma Expert.
|
||||
"None" means that the puzzles are unchanged from the original game.
|
||||
"""
|
||||
display_name = "Puzzle Randomization"
|
||||
option_sigma_normal = 0
|
||||
option_sigma_expert = 1
|
||||
option_umbra_variety = 3
|
||||
option_none = 2
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
|
||||
from .data import static_items as static_witness_items
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.item_definition_classes import (
|
||||
DoorItemDefinition,
|
||||
ItemCategory,
|
||||
@@ -155,16 +154,12 @@ class WitnessPlayerItems:
|
||||
"""
|
||||
output: Set[str] = set()
|
||||
if self._world.options.shuffle_symbols:
|
||||
output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
||||
discards_on = self._world.options.shuffle_discarded_panels
|
||||
mode = self._world.options.puzzle_randomization.current_key
|
||||
|
||||
if self._world.options.shuffle_discarded_panels:
|
||||
if self._world.options.puzzle_randomization == "sigma_expert":
|
||||
output.add("Arrows")
|
||||
else:
|
||||
output.add("Triangles")
|
||||
|
||||
# Replace progressive items with their parents.
|
||||
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
|
||||
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
|
||||
if discards_on:
|
||||
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
|
||||
|
||||
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
|
||||
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
|
||||
|
||||
@@ -87,12 +87,14 @@ class WitnessPlayerLogic:
|
||||
self.DIFFICULTY = world.options.puzzle_randomization
|
||||
|
||||
self.REFERENCE_LOGIC: StaticWitnessLogicObj
|
||||
if self.DIFFICULTY == "sigma_expert":
|
||||
if self.DIFFICULTY == "sigma_normal":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
||||
elif self.DIFFICULTY == "sigma_expert":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
|
||||
elif self.DIFFICULTY == "umbra_variety":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.umbra_variety
|
||||
elif self.DIFFICULTY == "none":
|
||||
self.REFERENCE_LOGIC = static_witness_logic.vanilla
|
||||
else:
|
||||
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
|
||||
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
|
||||
|
||||
@@ -30,6 +30,8 @@ class WitnessPlayerRegions:
|
||||
self.reference_logic = static_witness_logic.sigma_normal
|
||||
elif difficulty == "sigma_expert":
|
||||
self.reference_logic = static_witness_logic.sigma_expert
|
||||
elif difficulty == "umbra_variety":
|
||||
self.reference_logic = static_witness_logic.umbra_variety
|
||||
else:
|
||||
self.reference_logic = static_witness_logic.vanilla
|
||||
|
||||
|
||||
@@ -96,6 +96,39 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
|
||||
self.assert_can_beat_with_minimally(exact_requirement)
|
||||
|
||||
|
||||
class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
|
||||
options = {
|
||||
"shuffle_lasers": True,
|
||||
"mountain_lasers": 1,
|
||||
"victory_condition": "elevator",
|
||||
"early_symbol_item": False,
|
||||
"puzzle_randomization": "umbra_variety",
|
||||
}
|
||||
|
||||
def test_symbols_to_win(self) -> None:
|
||||
"""
|
||||
In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
|
||||
This requires a very specific set of symbol items per puzzle randomization mode.
|
||||
In this case, we check Variety Puzzles.
|
||||
"""
|
||||
|
||||
exact_requirement = {
|
||||
"Monastery Laser": 1,
|
||||
"Progressive Dots": 2,
|
||||
"Progressive Stars": 2,
|
||||
"Progressive Symmetry": 1,
|
||||
"Black/White Squares": 1,
|
||||
"Colored Squares": 1,
|
||||
"Shapers": 1,
|
||||
"Rotated Shapers": 1,
|
||||
"Eraser": 1,
|
||||
"Triangles": 1,
|
||||
"Arrows": 1,
|
||||
}
|
||||
|
||||
self.assert_can_beat_with_minimally(exact_requirement)
|
||||
|
||||
|
||||
class TestPanelsRequiredToWinElevator(WitnessTestBase):
|
||||
options = {
|
||||
"shuffle_lasers": True,
|
||||
|
||||
@@ -54,6 +54,7 @@ class TestMaxEntityShuffle(WitnessTestBase):
|
||||
|
||||
class TestPostgameGroupedDoors(WitnessTestBase):
|
||||
options = {
|
||||
"puzzle_randomization": "umbra_variety",
|
||||
"shuffle_postgame": True,
|
||||
"shuffle_discarded_panels": True,
|
||||
"shuffle_doors": "doors",
|
||||
|
||||
@@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
|
||||
{
|
||||
"puzzle_randomization": "none",
|
||||
},
|
||||
{
|
||||
"puzzle_randomization": "umbra_variety",
|
||||
}
|
||||
]
|
||||
|
||||
common_options = {
|
||||
@@ -63,12 +66,15 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
|
||||
self.assertFalse(self.get_items_by_name("Arrows", 1))
|
||||
self.assertTrue(self.get_items_by_name("Arrows", 2))
|
||||
self.assertFalse(self.get_items_by_name("Arrows", 3))
|
||||
self.assertTrue(self.get_items_by_name("Arrows", 4))
|
||||
|
||||
with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
|
||||
desert_discard = "0x17CE7"
|
||||
triangles = frozenset({frozenset({"Triangles"})})
|
||||
arrows = frozenset({frozenset({"Arrows"})})
|
||||
both = frozenset({frozenset({"Triangles", "Arrows"})})
|
||||
|
||||
self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
||||
self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
|
||||
self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
|
||||
self.assertEqual(self.multiworld.worlds[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)
|
||||
|
||||
@@ -29,7 +29,7 @@ class Category:
|
||||
mean_score = 0
|
||||
for key, value in yacht_weights[self.name, min(8, num_dice), min(8, num_rolls)].items():
|
||||
mean_score += key * value / 100000
|
||||
return mean_score * self.quantity
|
||||
return mean_score
|
||||
|
||||
|
||||
class ListState:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,7 @@ class YachtDiceWorld(World):
|
||||
|
||||
item_name_groups = item_groups
|
||||
|
||||
ap_world_version = "2.1.1"
|
||||
ap_world_version = "2.1.2"
|
||||
|
||||
def _get_yachtdice_data(self):
|
||||
return {
|
||||
@@ -190,7 +190,6 @@ class YachtDiceWorld(World):
|
||||
if self.frags_per_roll == 1:
|
||||
self.itempool += ["Roll"] * num_of_rolls_to_add # minus one because one is in start inventory
|
||||
else:
|
||||
self.itempool.append("Roll") # always add a full roll to make generation easier (will be early)
|
||||
self.itempool += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add)
|
||||
|
||||
already_items = len(self.itempool)
|
||||
@@ -231,13 +230,10 @@ class YachtDiceWorld(World):
|
||||
weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice
|
||||
weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll
|
||||
|
||||
extra_points_added = 0
|
||||
multipliers_added = 0
|
||||
items_added = 0
|
||||
|
||||
def get_item_to_add(weights, extra_points_added, multipliers_added, items_added):
|
||||
items_added += 1
|
||||
extra_points_added = [0] # make it a mutible type so we can change the value in the function
|
||||
step_score_multipliers_added = [0]
|
||||
|
||||
def get_item_to_add(weights, extra_points_added, step_score_multipliers_added):
|
||||
all_items = self.itempool + self.precollected
|
||||
dice_fragments_in_pool = all_items.count("Dice") * self.frags_per_dice + all_items.count("Dice Fragment")
|
||||
if dice_fragments_in_pool + 1 >= 9 * self.frags_per_dice:
|
||||
@@ -246,21 +242,18 @@ class YachtDiceWorld(World):
|
||||
if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll:
|
||||
weights["Roll"] = 0 # don't allow >= 6 rolls
|
||||
|
||||
# Don't allow too many multipliers
|
||||
if multipliers_added > 50:
|
||||
weights["Fixed Score Multiplier"] = 0
|
||||
weights["Step Score Multiplier"] = 0
|
||||
|
||||
# Don't allow too many extra points
|
||||
if extra_points_added > 300:
|
||||
if extra_points_added[0] > 400:
|
||||
weights["Points"] = 0
|
||||
|
||||
if step_score_multipliers_added[0] > 10:
|
||||
weights["Step Score Multiplier"] = 0
|
||||
|
||||
# if all weights are zero, allow to add fixed score multiplier, double category, points.
|
||||
if sum(weights.values()) == 0:
|
||||
if multipliers_added <= 50:
|
||||
weights["Fixed Score Multiplier"] = 1
|
||||
weights["Fixed Score Multiplier"] = 1
|
||||
weights["Double category"] = 1
|
||||
if extra_points_added <= 300:
|
||||
if extra_points_added[0] <= 400:
|
||||
weights["Points"] = 1
|
||||
|
||||
# Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item
|
||||
@@ -274,11 +267,10 @@ class YachtDiceWorld(World):
|
||||
return "Roll" if self.frags_per_roll == 1 else "Roll Fragment"
|
||||
elif which_item_to_add == "Fixed Score Multiplier":
|
||||
weights["Fixed Score Multiplier"] /= 1.05
|
||||
multipliers_added += 1
|
||||
return "Fixed Score Multiplier"
|
||||
elif which_item_to_add == "Step Score Multiplier":
|
||||
weights["Step Score Multiplier"] /= 1.1
|
||||
multipliers_added += 1
|
||||
step_score_multipliers_added[0] += 1
|
||||
return "Step Score Multiplier"
|
||||
elif which_item_to_add == "Double category":
|
||||
# Below entries are the weights to add each category.
|
||||
@@ -303,15 +295,15 @@ class YachtDiceWorld(World):
|
||||
choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0]
|
||||
if choice == "1 Point":
|
||||
weights["Points"] /= 1.01
|
||||
extra_points_added += 1
|
||||
extra_points_added[0] += 1
|
||||
return "1 Point"
|
||||
elif choice == "10 Points":
|
||||
weights["Points"] /= 1.1
|
||||
extra_points_added += 10
|
||||
extra_points_added[0] += 10
|
||||
return "10 Points"
|
||||
elif choice == "100 Points":
|
||||
weights["Points"] /= 2
|
||||
extra_points_added += 100
|
||||
extra_points_added[0] += 100
|
||||
return "100 Points"
|
||||
else:
|
||||
raise Exception("Unknown point value (Yacht Dice)")
|
||||
@@ -320,7 +312,7 @@ class YachtDiceWorld(World):
|
||||
|
||||
# adding 17 items as a start seems like the smartest way to get close to 1000 points
|
||||
for _ in range(17):
|
||||
self.itempool.append(get_item_to_add(weights, extra_points_added, multipliers_added, items_added))
|
||||
self.itempool.append(get_item_to_add(weights, extra_points_added, step_score_multipliers_added))
|
||||
|
||||
score_in_logic = dice_simulation_fill_pool(
|
||||
self.itempool + self.precollected,
|
||||
@@ -348,7 +340,7 @@ class YachtDiceWorld(World):
|
||||
else:
|
||||
# Keep adding items until a score of 1000 is in logic
|
||||
while score_in_logic < 1000:
|
||||
item_to_add = get_item_to_add(weights, extra_points_added, multipliers_added, items_added)
|
||||
item_to_add = get_item_to_add(weights, extra_points_added, step_score_multipliers_added)
|
||||
self.itempool.append(item_to_add)
|
||||
if item_to_add == "1 Point":
|
||||
score_in_logic += 1
|
||||
|
||||
Reference in New Issue
Block a user