diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 3ad29b007772..9a3a6d11217f 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -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 diff --git a/BaseClasses.py b/BaseClasses.py index 29264f34ab0f..0d4f34e51445 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -194,7 +194,9 @@ def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset( self.player_types[new_id] = NetUtils.SlotType.group world_type = AutoWorld.AutoWorldRegister.world_types[game] self.worlds[new_id] = world_type.create_group(self, new_id, players) - self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) + self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id]) + self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id]) + self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id]) self.player_name[new_id] = name new_group = self.groups[new_id] = Group(name=name, game=game, players=players, @@ -342,6 +344,8 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ region = Region("Menu", group_id, self, "ItemLink") self.regions.append(region) locations = region.locations + # ensure that progression items are linked first, then non-progression + self.itempool.sort(key=lambda item: item.advancement) for item in self.itempool: count = common_item_count.get(item.player, {}).get(item.name, 0) if count: @@ -692,17 +696,25 @@ def __init__(self, parent: MultiWorld): 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() @@ -710,7 +722,7 @@ def update_reachable_regions(self, player: int): 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" + assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region" reachable_regions.add(new_region) blocked_connections.remove(connection) blocked_connections.update(new_region.exits) @@ -722,6 +734,29 @@ def update_reachable_regions(self, player: int): 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()} @@ -913,6 +948,7 @@ def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) self.player = player def can_reach(self, state: CollectionState) -> bool: + assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region" if self.parent_region.can_reach(state) and self.access_rule(state): if not self.hide_path and not self in state.path: state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) @@ -1133,7 +1169,7 @@ def can_fill(self, state: CollectionState, item: Item, check_access: bool = True def can_reach(self, state: CollectionState) -> bool: # Region.can_reach is just a cache lookup, so placing it first for faster abort on average - assert self.parent_region, "Can't reach location without region" + assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region" return self.parent_region.can_reach(state) and self.access_rule(state) def place_locked_item(self, item: Item): @@ -1176,7 +1212,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. diff --git a/BizHawkClient.py b/BizHawkClient.py index 86c8e5197e3f..743785b25f16 100644 --- a/BizHawkClient.py +++ b/BizHawkClient.py @@ -1,9 +1,10 @@ from __future__ import annotations +import sys import ModuleUpdate ModuleUpdate.update() from worlds._bizhawk.context import launch if __name__ == "__main__": - launch() + launch(*sys.argv[1:]) diff --git a/CommonClient.py b/CommonClient.py index 750bee80bd70..1aedd518b4f8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -45,10 +45,21 @@ def get_ssl_context(): class ClientCommandProcessor(CommandProcessor): + """ + The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called + when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit". + + The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first + space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw + and method("one", "two", "three") without. + + In addition all docstrings for command methods will be displayed to the user on launch and when using "/help" + """ def __init__(self, ctx: CommonContext): self.ctx = ctx def output(self, text: str): + """Helper function to abstract logging to the CommonClient UI""" logger.info(text) def _cmd_exit(self) -> bool: @@ -164,13 +175,14 @@ def _cmd_ready(self): async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") def default(self, raw: str): + """The default message parser to be used when parsing any messages that do not match a command""" raw = self.ctx.on_user_say(raw) if raw: async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") class CommonContext: - # Should be adjusted as needed in subclasses + # The following attributes are used to Connect and should be adjusted as needed in subclasses tags: typing.Set[str] = {"AP"} game: typing.Optional[str] = None items_handling: typing.Optional[int] = None @@ -429,7 +441,10 @@ async def get_username(self): self.auth = await self.console_input() async def send_connect(self, **kwargs: typing.Any) -> None: - """ send `Connect` packet to log in to server """ + """ + Send a `Connect` packet to log in to the server, + additional keyword args can override any value in the connection packet + """ payload = { 'cmd': 'Connect', 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, @@ -459,6 +474,7 @@ def cancel_autoreconnect(self) -> bool: return False def slot_concerns_self(self, slot) -> bool: + """Helper function to abstract player groups, should be used instead of checking slot == self.slot directly.""" if slot == self.slot: return True if slot in self.slot_info: @@ -466,6 +482,7 @@ def slot_concerns_self(self, slot) -> bool: return False def is_echoed_chat(self, print_json_packet: dict) -> bool: + """Helper function for filtering out messages sent by self.""" return print_json_packet.get("type", "") == "Chat" \ and print_json_packet.get("team", None) == self.team \ and print_json_packet.get("slot", None) == self.slot @@ -497,13 +514,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]: """Gets called before sending a Say to the server from the user. Returned text is sent, or sending is aborted if None is returned.""" return text - + def on_ui_command(self, text: str) -> None: """Gets called by kivy when the user executes a command starting with `/` or `!`. The command processor is still called; this is just intended for command echoing.""" self.ui.print_json([{"text": text, "type": "color", "color": "orange"}]) def update_permissions(self, permissions: typing.Dict[str, int]): + """Internal method to parse and save server permissions from RoomInfo""" for permission_name, permission_flag in permissions.items(): try: flag = Permission(permission_flag) @@ -613,6 +631,7 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: logger.info(f"DeathLink: Received from {data['source']}") async def send_death(self, death_text: str = ""): + """Helper function to send a deathlink using death_text as the unique death cause string.""" if self.server and self.server.socket: logger.info("DeathLink: Sending death to your friends...") self.last_death_link = time.time() @@ -626,6 +645,7 @@ async def send_death(self, death_text: str = ""): }]) async def update_death_link(self, death_link: bool): + """Helper function to set Death Link connection tag on/off and update the connection if already connected.""" old_tags = self.tags.copy() if death_link: self.tags.add("DeathLink") @@ -635,7 +655,7 @@ async def update_death_link(self, death_link: bool): await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]: - """Displays an error messagebox""" + """Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework""" if not self.ui: return None title = title or "Error" @@ -662,17 +682,19 @@ def handle_connection_loss(self, msg: str) -> None: 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) -> typing.Type["kvui.GameManager"]: + """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): @@ -985,6 +1007,7 @@ async def console_loop(ctx: CommonContext): def get_base_parser(description: typing.Optional[str] = None): + """Base argument parser to be reused for components subclassing off of CommonClient""" import argparse parser = argparse.ArgumentParser(description=description) parser.add_argument('--connect', default=None, help='Address of the multiworld host.') @@ -994,7 +1017,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,16 +1056,21 @@ async def main(args): 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) + # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost if args.url: url = urllib.parse.urlparse(args.url) - args.connect = url.netloc - if url.username: - args.name = urllib.parse.unquote(url.username) - if url.password: - args.password = urllib.parse.unquote(url.password) + if url.scheme == "archipelago": + args.connect = url.netloc + if url.username: + args.name = urllib.parse.unquote(url.username) + if url.password: + args.password = urllib.parse.unquote(url.password) + else: + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + # use colorama to display colored text highlighting on windows colorama.init() asyncio.run(main(args)) @@ -1051,4 +1079,4 @@ async def main(args): if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING - run_as_textclient() + run_as_textclient(*sys.argv[1:]) # default value for parse_args diff --git a/Fill.py b/Fill.py index e2fcff00358e..706cca657457 100644 --- a/Fill.py +++ b/Fill.py @@ -475,28 +475,26 @@ def mark_for_locking(location: Location): nonlocal lock_later lock_later.append(location) + single_player = multiworld.players == 1 and not multiworld.groups + if prioritylocations: # "priority fill" fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, - single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking, - name="Priority") + single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority") accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: # "advancement/progression fill" if panic_method == "swap": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=True, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True, + name="Progression", single_player_placement=single_player) elif panic_method == "raise": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + name="Progression", single_player_placement=single_player) elif panic_method == "start_inventory": - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, - swap=False, allow_partial=True, - name="Progression", single_player_placement=multiworld.players == 1) + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, + allow_partial=True, name="Progression", single_player_placement=single_player) if progitempool: for item in progitempool: logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") diff --git a/Generate.py b/Generate.py index 6220c0eb8188..52babdf18839 100644 --- a/Generate.py +++ b/Generate.py @@ -43,10 +43,10 @@ def mystery_argparse(): parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--meta_file_path', default=defaults.meta_file_path) parser.add_argument('--log_level', default='info', help='Sets log level') - parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0), - help='Output rolled mystery results to yaml up to specified number (made for async multiworld)') - parser.add_argument('--plando', default=defaults.plando_options, - help='List of options that can be set manually. Can be combined, for example "bosses, items"') + parser.add_argument("--csv_output", action="store_true", + help="Output rolled player options to csv (made for async multiworld).") + parser.add_argument("--plando", default=defaults.plando_options, + help="List of options that can be set manually. Can be combined, for example \"bosses, items\"") parser.add_argument("--skip_prog_balancing", action="store_true", help="Skip progression balancing step during generation.") parser.add_argument("--skip_output", action="store_true", @@ -155,6 +155,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.outputpath = args.outputpath erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output + erargs.name = {} + erargs.csv_output = args.csv_output settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) @@ -202,7 +204,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: if path == args.weights_file_path: # if name came from the weights file, just use base player name erargs.name[player] = f"Player{player}" - elif not erargs.name[player]: # if name was not specified, generate it from filename + elif player not in erargs.name: # if name was not specified, generate it from filename erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = handle_name(erargs.name[player], player, name_counter) @@ -215,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}") - if args.yaml_output: - import yaml - important = {} - for option, player_settings in vars(erargs).items(): - if type(player_settings) == dict: - if all(type(value) != list for value in player_settings.values()): - if len(player_settings.values()) > 1: - important[option] = {player: value for player, value in player_settings.items() if - player <= args.yaml_output} - else: - logging.debug(f"No player settings defined for option '{option}'") - - else: - if player_settings != "": # is not empty name - important[option] = player_settings - else: - logging.debug(f"No player settings defined for option '{option}'") - if args.outputpath: - os.makedirs(args.outputpath, exist_ok=True) - with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: - yaml.dump(important, f) - return erargs, seed diff --git a/Launcher.py b/Launcher.py index 6b66b2a3a671..42f93547cc9d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -16,10 +16,11 @@ 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 @@ def update_settings(): ]) -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() @@ -322,12 +401,16 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None): init_logging('Launcher') Utils.freeze_support() multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work - parser = argparse.ArgumentParser(description='Archipelago Launcher') + parser = argparse.ArgumentParser( + description='Archipelago Launcher', + usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]" + ) 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()) diff --git a/LinksAwakeningClient.py b/LinksAwakeningClient.py index a51645feac92..298788098d9e 100644 --- a/LinksAwakeningClient.py +++ b/LinksAwakeningClient.py @@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext): def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None: self.client = LinksAwakeningClient() + self.slot_data = {} + if magpie: self.magpie_enabled = True self.magpie = MagpieBridge() @@ -564,6 +566,8 @@ async def server_auth(self, password_requested: bool = False): def on_package(self, cmd: str, args: dict): if cmd == "Connected": self.game = self.slot_info[self.slot].game + self.slot_data = args.get("slot_data", {}) + # TODO - use watcher_event if cmd == "ReceivedItems": for index, item in enumerate(args["items"], start=args["index"]): @@ -628,6 +632,7 @@ async def deathlink(): self.magpie.set_checks(self.client.tracker.all_checks) await self.magpie.set_item_tracker(self.client.item_tracker) await self.magpie.send_gps(self.client.gps_tracker) + self.magpie.slot_data = self.slot_data except Exception: # Don't let magpie errors take out the client pass diff --git a/Main.py b/Main.py index c931e22145a5..5a0f5c98bcc4 100644 --- a/Main.py +++ b/Main.py @@ -46,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.sprite_pool = args.sprite_pool.copy() multiworld.set_options(args) + if args.csv_output: + from Options import dump_player_options + dump_player_options(multiworld) multiworld.set_item_links() multiworld.state = CollectionState(multiworld) logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index ed041bef4604..f49182bb7863 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -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) diff --git a/MultiServer.py b/MultiServer.py index b7c0e0f74555..e0b137fd68ce 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -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 @@ def get_datetime_second(): 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 @@ def _cmd_countdown(self, seconds: str = "10") -> bool: 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 @@ def _cmd_send_multiple(self, amount: typing.Union[int, str], player_name: str, * 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) diff --git a/NetUtils.py b/NetUtils.py index c451fa3f8460..4776b228db17 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -273,7 +273,8 @@ def _handle_color(self, node: JSONMessagePart): color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, - 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47} + 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47, + 'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors def color_code(*args): diff --git a/Options.py b/Options.py index ecde6275f1ea..aa6f175fa58d 100644 --- a/Options.py +++ b/Options.py @@ -8,16 +8,17 @@ import random import typing import enum +from collections import defaultdict from copy import deepcopy from dataclasses import dataclass from schema import And, Optional, Or, Schema from typing_extensions import Self -from Utils import get_fuzzy_results, is_iterable_except_str +from Utils import get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: - from BaseClasses import PlandoOptions + from BaseClasses import MultiWorld, PlandoOptions from worlds.AutoWorld import World import pathlib @@ -973,7 +974,19 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: + if isinstance(at, dict): + if at: + at = random.choices(list(at.keys()), + weights=list(at.values()), k=1)[0] + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") given_text = text.get("text", []) + if isinstance(given_text, dict): + if not given_text: + given_text = [] + else: + given_text = random.choices(list(given_text.keys()), + weights=list(given_text.values()), k=1) if isinstance(given_text, str): given_text = [given_text] texts.append(PlandoText( @@ -981,6 +994,8 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: given_text, text.get("percentage", 100) )) + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") elif isinstance(text, PlandoText): if random.random() < float(text.percentage/100): texts.append(text) @@ -1321,7 +1336,7 @@ class PriorityLocations(LocationSet): class DeathLink(Toggle): - """When you die, everyone dies. Of course the reverse is true too.""" + """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" display_name = "Death Link" rich_text_doc = True @@ -1518,3 +1533,42 @@ def yaml_dump_scalar(scalar) -> str: with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) + + +def dump_player_options(multiworld: MultiWorld) -> None: + from csv import DictWriter + + game_players = defaultdict(list) + for player, game in multiworld.game.items(): + game_players[game].append(player) + game_players = dict(sorted(game_players.items())) + + output = [] + per_game_option_names = [ + getattr(option, "display_name", option_key) + for option_key, option in PerGameCommonOptions.type_hints.items() + ] + all_option_names = per_game_option_names.copy() + for game, players in game_players.items(): + game_option_names = per_game_option_names.copy() + for player in players: + world = multiworld.worlds[player] + player_output = { + "Game": multiworld.game[player], + "Name": multiworld.get_player_name(player), + } + output.append(player_output) + for option_key, option in world.options_dataclass.type_hints.items(): + if issubclass(Removed, option): + continue + display_name = getattr(option, "display_name", option_key) + player_output[display_name] = getattr(world.options, option_key).current_option_name + if display_name not in game_option_names: + all_option_names.append(display_name) + game_option_names.append(display_name) + + with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file: + fields = ["Game", "Name", *all_option_names] + writer = DictWriter(file, fields) + writer.writeheader() + writer.writerows(output) diff --git a/Utils.py b/Utils.py index f89330cf7c65..d6709431d32c 100644 --- a/Utils.py +++ b/Utils.py @@ -46,7 +46,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.5.0" +__version__ = "0.5.1" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/WargrooveClient.py b/WargrooveClient.py index 39da044d659c..f9971f7a6c05 100644 --- a/WargrooveClient.py +++ b/WargrooveClient.py @@ -267,9 +267,7 @@ class WargrooveManager(GameManager): def build(self): container = super().build() - panel = TabbedPanelItem(text="Wargroove") - panel.content = self.build_tracker() - self.tabs.add_widget(panel) + self.add_client_tab("Wargroove", self.build_tracker()) return container def build_tracker(self) -> TrackerLayout: diff --git a/WebHost.py b/WebHost.py index 08ef3c430795..e597de24763d 100644 --- a/WebHost.py +++ b/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() diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 4003243a281d..cf05e87374ab 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -1,51 +1,15 @@ """API endpoints package.""" from typing import List, Tuple -from uuid import UUID -from flask import Blueprint, abort, url_for +from flask import Blueprint -import worlds.Files -from ..models import Room, Seed +from ..models import Seed api_endpoints = Blueprint('api', __name__, url_prefix="/api") -# unsorted/misc endpoints - def get_players(seed: Seed) -> List[Tuple[str, str]]: return [(slot.player_name, slot.game) for slot in seed.slots] -@api_endpoints.route('/room_status/') -def room_info(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - - def supports_apdeltapatch(game: str): - return game in worlds.Files.AutoPatchRegister.patch_types - downloads = [] - for slot in sorted(room.seed.slots): - if slot.data and not supports_apdeltapatch(slot.game): - slot_download = { - "slot": slot.player_id, - "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) - } - downloads.append(slot_download) - elif slot.data: - slot_download = { - "slot": slot.player_id, - "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) - } - downloads.append(slot_download) - return { - "tracker": room.tracker, - "players": get_players(room.seed), - "last_port": room.last_port, - "last_activity": room.last_activity, - "timeout": room.timeout, - "downloads": downloads, - } - - -from . import generate, user, datapackage # trigger registration +from . import datapackage, generate, room, user # trigger registration diff --git a/WebHostLib/api/room.py b/WebHostLib/api/room.py new file mode 100644 index 000000000000..9337975695b2 --- /dev/null +++ b/WebHostLib/api/room.py @@ -0,0 +1,42 @@ +from typing import Any, Dict +from uuid import UUID + +from flask import abort, url_for + +import worlds.Files +from . import api_endpoints, get_players +from ..models import Room + + +@api_endpoints.route('/room_status/') +def room_info(room_id: UUID) -> Dict[str, Any]: + room = Room.get(id=room_id) + if room is None: + return abort(404) + + def supports_apdeltapatch(game: str) -> bool: + return game in worlds.Files.AutoPatchRegister.patch_types + + downloads = [] + for slot in sorted(room.seed.slots): + if slot.data and not supports_apdeltapatch(slot.game): + slot_download = { + "slot": slot.player_id, + "download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id) + } + downloads.append(slot_download) + elif slot.data: + slot_download = { + "slot": slot.player_id, + "download": url_for("download_patch", patch_id=slot.id, room_id=room.id) + } + downloads.append(slot_download) + + return { + "tracker": room.tracker, + "players": get_players(room.seed), + "last_port": room.last_port, + "last_activity": room.last_activity, + "timeout": room.timeout, + "downloads": downloads, + } diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index ccffc40b384d..a2eef108b0a1 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -72,6 +72,14 @@ def __init__(self, static_server_data: dict, logger: logging.Logger): 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 @@ async def start_room(room_id): 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 @@ async def start_room(room_id): 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 _done(self, task: asyncio.Future): 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) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index a12dc0f4ae14..dbe7dd958910 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -134,6 +134,7 @@ def task(): {"bosses", "items", "connections", "texts"})) erargs.skip_prog_balancing = False erargs.skip_output = False + erargs.csv_output = False name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 01c1ad84a707..4784fcd9da63 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -132,26 +132,41 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: return "Access Denied", 403 -@app.route('/room/', methods=['GET', 'POST']) +@app.post("/room/") +def host_room_command(room: UUID): + room: Room = Room.get(id=room) + if room is None: + return abort(404) + + if room.owner == session["_id"]: + cmd = request.form["cmd"] + if cmd: + Command(room=room, commandtext=cmd) + commit() + return redirect(url_for("host_room", room=room.id)) + + +@app.get("/room/") def host_room(room: UUID): room: Room = Room.get(id=room) if room is None: return abort(404) - if request.method == "POST": - if room.owner == session["_id"]: - cmd = request.form["cmd"] - if cmd: - Command(room=room, commandtext=cmd) - commit() - return redirect(url_for("host_room", room=room.id)) now = datetime.datetime.utcnow() # indicate that the page should reload to get the assigned port - should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) + should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)) + or room.last_activity < now - datetime.timedelta(seconds=room.timeout)) with db_session: room.last_activity = now # will trigger a spinup, if it's not already running - def get_log(max_size: int = 1024000) -> str: + browser_tokens = "Mozilla", "Chrome", "Safari" + automated = ("update" in request.args + or "Discordbot" in request.user_agent.string + or not any(browser_token in request.user_agent.string for browser_token in browser_tokens)) + + def get_log(max_size: int = 0 if automated else 1024000) -> str: + if max_size == 0: + return "…" try: with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: raw_size = 0 diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 3452c9d416db..c593cd63df7e 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -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 diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/faq_en.md index fb1ccd2d6f4a..e64535b42d03 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/faq_en.md @@ -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. diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index 827f74c04df7..625b78cc5d3f 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -58,3 +58,28 @@ overflow-y: auto; max-height: 400px; } + +.loader{ + display: inline-block; + visibility: hidden; + margin-left: 5px; + width: 40px; + aspect-ratio: 4; + --_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0); + background: + var(--_g) 0 50%, + var(--_g) 50% 50%, + var(--_g) 100% 50%; + background-size: calc(100%/3) 100%; + animation: l7 1s infinite linear; +} + +.loader.loading{ + visibility: visible; +} + +@keyframes l7{ + 33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%} + 50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%} + 66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 } +} diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 5a533204083b..947cf2837278 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -99,14 +99,18 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} {% else %} - {{ player_names_with_alias[(team, hint.finding_player)] }} + + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} {% if hint.receiving_player == player %} {{ player_names_with_alias[(team, hint.receiving_player)] }} {% else %} - {{ player_names_with_alias[(team, hint.receiving_player)] }} + + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index fa8e26c2cbf8..8e76dafc12fa 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -19,28 +19,30 @@ {% block body %} {% include 'header/grassHeader.html' %}
- {% if room.owner == session["_id"] %} - Room created from Seed #{{ room.seed.id|suuid }} -
- {% endif %} - {% if room.tracker %} - This room has a Multiworld Tracker - and a Sphere Tracker enabled. -
- {% endif %} - The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. - Should you wish to continue later, - anyone can simply refresh this page and the server will resume.
- {% if room.last_port == -1 %} - There was an error hosting this Room. Another attempt will be made on refreshing this page. - The most likely failure reason is that the multiworld is too old to be loaded now. - {% elif room.last_port %} - You can connect to this room by using - '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' - - in the client.
- {% endif %} + + {% if room.owner == session["_id"] %} + Room created from Seed #{{ room.seed.id|suuid }} +
+ {% endif %} + {% if room.tracker %} + This room has a Multiworld Tracker + and a Sphere Tracker enabled. +
+ {% endif %} + The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. + Should you wish to continue later, + anyone can simply refresh this page and the server will resume.
+ {% if room.last_port == -1 %} + There was an error hosting this Room. Another attempt will be made on refreshing this page. + The most likely failure reason is that the multiworld is too old to be loaded now. + {% elif room.last_port %} + You can connect to this room by using + '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' + + in the client.
+ {% endif %} +
{{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %}
@@ -49,6 +51,7 @@ +
@@ -62,6 +65,7 @@ let url = '{{ url_for('display_log', room = room.id) }}'; let bytesReceived = {{ log_len }}; let updateLogTimeout; + let updateLogImmediately = false; let awaitingCommandResponse = false; let logger = document.getElementById("logger"); @@ -78,29 +82,36 @@ async function updateLog() { try { - let res = await fetch(url, { - headers: { - 'Range': `bytes=${bytesReceived}-`, - } - }); - if (res.ok) { - let text = await res.text(); - if (text.length > 0) { - awaitingCommandResponse = false; - if (bytesReceived === 0 || res.status !== 206) { - logger.innerHTML = ''; + if (!document.hidden) { + updateLogImmediately = false; + let res = await fetch(url, { + headers: { + 'Range': `bytes=${bytesReceived}-`, } - if (res.status !== 206) { - bytesReceived = 0; - } else { - bytesReceived += new Blob([text]).size; + }); + if (res.ok) { + let text = await res.text(); + if (text.length > 0) { + awaitingCommandResponse = false; + if (bytesReceived === 0 || res.status !== 206) { + logger.innerHTML = ''; + } + if (res.status !== 206) { + bytesReceived = 0; + } else { + bytesReceived += new Blob([text]).size; + } + if (logger.innerHTML.endsWith('…')) { + logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1); + } + logger.appendChild(document.createTextNode(text)); + scrollToBottom(logger); + let loader = document.getElementById("command-form").getElementsByClassName("loader")[0]; + loader.classList.remove("loading"); } - if (logger.innerHTML.endsWith('…')) { - logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1); - } - logger.appendChild(document.createTextNode(text)); - scrollToBottom(logger); } + } else { + updateLogImmediately = true; } } finally { @@ -125,20 +136,62 @@ }); ev.preventDefault(); // has to happen before first await form.reset(); - let res = await req; - if (res.ok || res.type === 'opaqueredirect') { - awaitingCommandResponse = true; - window.clearTimeout(updateLogTimeout); - updateLogTimeout = window.setTimeout(updateLog, 100); - } else { - window.alert(res.statusText); + let loader = form.getElementsByClassName("loader")[0]; + loader.classList.add("loading"); + try { + let res = await req; + if (res.ok || res.type === 'opaqueredirect') { + awaitingCommandResponse = true; + window.clearTimeout(updateLogTimeout); + updateLogTimeout = window.setTimeout(updateLog, 100); + } else { + loader.classList.remove("loading"); + window.alert(res.statusText); + } + } catch (e) { + console.error(e); + loader.classList.remove("loading"); + window.alert(e.message); } } document.getElementById("command-form").addEventListener("submit", postForm); updateLogTimeout = window.setTimeout(updateLog, 1000); logger.scrollTop = logger.scrollHeight; + document.addEventListener("visibilitychange", () => { + if (!document.hidden && updateLogImmediately) { + updateLog(); + } + }) {% endif %} +
{% endblock %} diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 7bbb894de090..6b2a4b0ed784 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} {{ patch.player_id }} - {{ patch.player_name }} + {{ patch.player_name }} {{ patch.game }} {% if patch.data %} diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html index 3603d4112d20..4e3747f4f952 100644 --- a/WebHostLib/templates/userContent.html +++ b/WebHostLib/templates/userContent.html @@ -1,5 +1,21 @@ {% extends 'tablepage.html' %} +{%- macro games(slots) -%} + {%- set gameList = [] -%} + {%- set maxGamesToShow = 10 -%} + + {%- for slot in (slots|list|sort(attribute="player_id"))[:maxGamesToShow] -%} + {% set player = "#" + slot["player_id"]|string + " " + slot["player_name"] + " : " + slot["game"] -%} + {% set _ = gameList.append(player) -%} + {%- endfor -%} + + {%- if slots|length > maxGamesToShow -%} + {% set _ = gameList.append("... and " + (slots|length - maxGamesToShow)|string + " more") -%} + {%- endif -%} + + {{ gameList|join('\n') }} +{%- endmacro -%} + {% block head %} {{ super() }} User Content @@ -33,10 +49,12 @@

Your Rooms

{{ room.seed.id|suuid }} {{ room.id|suuid }} - {{ room.seed.slots|length }} + + {{ room.seed.slots|length }} + {{ room.creation_time.strftime("%Y-%m-%d %H:%M") }} {{ room.last_activity.strftime("%Y-%m-%d %H:%M") }} - Delete next maintenance. + Delete next maintenance. {% endfor %} @@ -60,16 +78,21 @@

Your Seeds

{% for seed in seeds %} {{ seed.id|suuid }} - {% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %} + + {% if seed.multidata %} + {{ seed.slots|length }} + {% else %} + 1 + {% endif %} {{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }} - Delete next maintenance. + Delete next maintenance. {% endfor %} {% else %} - You have no generated any seeds yet! + You have not generated any seeds yet! {% endif %} diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index cd1e859af951..ee7fd7ed863b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -46,7 +46,7 @@ /worlds/clique/ @ThePhar # Dark Souls III -/worlds/dark_souls_3/ @Marechal-L +/worlds/dark_souls_3/ @Marechal-L @nex3 # Donkey Kong Country 3 /worlds/dkc3/ @PoryGone @@ -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 diff --git a/docs/options api.md b/docs/options api.md index 7e479809ee6a..d48a56d6c76d 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -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 diff --git a/docs/running from source.md b/docs/running from source.md index 34083a603d1b..a161265fcb74 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -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 diff --git a/docs/world api.md b/docs/world api.md index 6551f2260416..bf09d965f11d 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -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). diff --git a/docs/world maintainer.md b/docs/world maintainer.md index 15fa46a1efcd..17aacdf8c269 100644 --- a/docs/world maintainer.md +++ b/docs/world maintainer.md @@ -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 diff --git a/inno_setup.iss b/inno_setup.iss index 3bb76fc40abe..38e655d917c1 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -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 diff --git a/kvui.py b/kvui.py index 65cf52c7a4aa..536dce12208e 100644 --- a/kvui.py +++ b/kvui.py @@ -536,9 +536,8 @@ def connect_bar_validate(sender): # show Archipelago tab if other logging is present self.tabs.add_widget(panel) - hint_panel = TabbedPanelItem(text="Hints") - self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser) - self.tabs.add_widget(hint_panel) + hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser)) + self.log_panels["Hints"] = hint_panel.content if len(self.logging_pairs) == 1: self.tabs.default_tab_text = "Archipelago" @@ -572,6 +571,14 @@ def connect_bar_validate(sender): return self.container + def add_client_tab(self, title: str, content: Widget) -> Widget: + """Adds a new tab to the client window with a given title, and provides a given Widget as its content. + Returns the new tab widget, with the provided content being placed on the tab as content.""" + new_tab = TabbedPanelItem(text=title) + new_tab.content = content + self.tabs.add_widget(new_tab) + return new_tab + def update_texts(self, dt): if hasattr(self.tabs.content.children[0], "fix_heights"): self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream diff --git a/requirements.txt b/requirements.txt index db4f5445036a..6fe14c9f32ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py index e9dae41dd06f..4aa83e3b1c6c 100644 --- a/test/webhost/test_host_room.py +++ b/test/webhost/test_host_room.py @@ -131,7 +131,8 @@ def test_host_room_own(self) -> None: f.write(text) with self.app.app_context(), self.app.test_request_context(): - response = self.client.get(url_for("host_room", room=self.room_id)) + response = self.client.get(url_for("host_room", room=self.room_id), + headers={"User-Agent": "Mozilla/5.0"}) response_text = response.get_data(True) self.assertEqual(response.status_code, 200) self.assertIn("href=\"/seed/", response_text) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index af067e5cb8a6..f7dae2b92750 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -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 @@ -334,7 +342,7 @@ def __getattr__(self, item: str) -> Any: # overridable methods that get called by Main.py, sorted by execution order # can also be implemented as a classmethod and called "stage_", - # in that case the MultiWorld object is passed as an argument, and it gets called once for the entire multiworld. + # in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld. # An example of this can be found in alttp as stage_pre_fill @classmethod diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index d127bbea36ed..fe6e44bb308e 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -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 @@ def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_ 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 @@ def __repr__(self): processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None): +def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None: 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 @@ def __call__(self, path: str) -> bool: 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, name="TextClient", args=args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py index 74f2954b984b..3627f385c2d3 100644 --- a/worlds/_bizhawk/__init__.py +++ b/worlds/_bizhawk/__init__.py @@ -223,8 +223,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None: raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") -async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]], - guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]: +async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]], + guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]: """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected value. @@ -266,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[ return ret -async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]: +async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]: """Reads data at 1 or more addresses. Items in `read_list` should be organized `(address, size, domain)` where @@ -278,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int return await guarded_read(ctx, read_list, []) -async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]], - guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool: +async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]], + guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool: """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. Items in `write_list` should be organized `(address, value, domain)` where @@ -316,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl return True -async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None: +async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None: """Writes data to 1 or more addresses. Items in write_list should be organized `(address, value, domain)` where diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py index 00370c277a17..415b663e60af 100644 --- a/worlds/_bizhawk/client.py +++ b/worlds/_bizhawk/client.py @@ -15,7 +15,7 @@ def launch_client(*args) -> None: from .context import launch - launch_subprocess(launch, name="BizHawkClient") + launch_subprocess(launch, name="BizHawkClient", args=args) component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 234faf3b65cf..2a3965a54fcd 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -59,14 +59,10 @@ def __init__(self, server_address: Optional[str], password: Optional[str]): self.bizhawk_ctx = BizHawkContext() self.watcher_timeout = 0.5 - def run_gui(self): - from kvui import GameManager - - class BizHawkManager(GameManager): - base_title = "Archipelago BizHawk Client" - - self.ui = BizHawkManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + def make_gui(self): + ui = super().make_gui() + ui.base_title = "Archipelago BizHawk Client" + return ui def on_package(self, cmd, args): if cmd == "Connected": @@ -243,11 +239,11 @@ async def _patch_and_run_game(patch_file: str): logger.exception(exc) -def launch() -> None: +def launch(*launch_args) -> None: async def main(): parser = get_base_parser() parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") - args = parser.parse_args() + args = parser.parse_args(launch_args) ctx = BizHawkClientContext(args.connect, args.password) ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") diff --git a/worlds/ahit/Client.py b/worlds/ahit/Client.py index 2cd67e468294..cbb5f2a13d1f 100644 --- a/worlds/ahit/Client.py +++ b/worlds/ahit/Client.py @@ -4,7 +4,7 @@ import functools from copy import deepcopy from typing import List, Any, Iterable -from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer from MultiServer import Endpoint from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser @@ -101,12 +101,35 @@ def update_items(self): def on_package(self, cmd: str, args: dict): if cmd == "Connected": - self.connected_msg = encode([args]) + json = args + # This data is not needed and causes the game to freeze for long periods of time in large asyncs. + if "slot_info" in json.keys(): + json["slot_info"] = {} + if "players" in json.keys(): + me: NetworkPlayer + for n in json["players"]: + if n.slot == json["slot"] and n.team == json["team"]: + me = n + break + + # Only put our player info in there as we actually need it + json["players"] = [me] + if DEBUG: + print(json) + self.connected_msg = encode([json]) if self.awaiting_info: self.server_msgs.append(self.room_info) self.update_items() self.awaiting_info = False + elif cmd == "RoomUpdate": + # Same story as above + json = args + if "players" in json.keys(): + json["players"] = [] + + self.server_msgs.append(encode(json)) + elif cmd == "ReceivedItems": if args["index"] == 0: self.full_inventory.clear() @@ -166,6 +189,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None): await ctx.disconnect_proxy() break + if ctx.auth: + name = msg.get("name", "") + if name != "" and name != ctx.auth: + logger.info("Aborting proxy connection: player name mismatch from save file") + logger.info(f"Expected: {ctx.auth}, got: {name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - player name mismatch"}]}]) + await ctx.send_msgs_proxy(text) + await ctx.disconnect_proxy() + break + if ctx.connected_msg and ctx.is_connected(): await ctx.send_msgs_proxy(ctx.connected_msg) ctx.update_items() diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index ef74cadcaa53..ce339c7c19bb 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"): for name in annoying_dws: world.excluded_dws.append(name) - if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses: + if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses: for name in death_wishes: world.excluded_bonuses.append(name) - elif world.options.DWExcludeAnnoyingBonuses: + if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses: for name in annoying_bonuses: world.excluded_bonuses.append(name) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 8cb3782bdec6..c70f08b475eb 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -968,40 +968,35 @@ def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> R def create_thug_shops(world: "HatInTimeWorld"): min_items: int = world.options.NyakuzaThugMinShopItems.value max_items: int = world.options.NyakuzaThugMaxShopItems.value - count = -1 - step = 0 - old_name = "" + + thug_location_counts: Dict[str, int] = {} for key, data in shop_locations.items(): - if data.nyakuza_thug == "": + thug_name = data.nyakuza_thug + if thug_name == "": + # Different shop type. continue - if old_name != "" and old_name == data.nyakuza_thug: - continue + if thug_name not in world.nyakuza_thug_items: + shop_item_count = world.random.randint(min_items, max_items) + world.nyakuza_thug_items[thug_name] = shop_item_count + else: + shop_item_count = world.nyakuza_thug_items[thug_name] - try: - if world.nyakuza_thug_items[data.nyakuza_thug] <= 0: - continue - except KeyError: - pass + if shop_item_count <= 0: + continue - if count == -1: - count = world.random.randint(min_items, max_items) - world.nyakuza_thug_items.setdefault(data.nyakuza_thug, count) - if count <= 0: - continue + location_count = thug_location_counts.setdefault(thug_name, 0) + if location_count >= shop_item_count: + # Already created all the locations for this thug. + continue - if count >= 1: - region = world.multiworld.get_region(data.region, world.player) - loc = HatInTimeLocation(world.player, key, data.id, region) - region.locations.append(loc) - world.shop_locs.append(loc.name) - - step += 1 - if step >= count: - old_name = data.nyakuza_thug - step = 0 - count = -1 + # Create the shop location. + region = world.multiworld.get_region(data.region, world.player) + loc = HatInTimeLocation(world.player, key, data.id, region) + region.locations.append(loc) + world.shop_locs.append(loc.name) + thug_location_counts[thug_name] = location_count + 1 def create_events(world: "HatInTimeWorld") -> int: diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index b716b793a797..183248a0e6d7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"): lambda state: can_use_hat(state, world, HatType.ICE), "or") # Moderate: Clock Tower Chest + Ruined Tower with nothing - add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) - add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) # Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: @@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"): if world.is_dlc1(): # Moderate: clear Rock the Boat without Ice Hat - add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) - add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) # Moderate: clear Deep Sea without Ice Hat set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), @@ -855,6 +855,9 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): for entrance in regions["Time Rift - Alpine Skyline"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + if entrance.parent_region.name == "Alpine Free Roam": + add_rule(entrance, + lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True)) if world.is_dlc1(): for entrance in regions["Time Rift - Balcony"].entrances: @@ -933,6 +936,9 @@ def set_default_rift_rules(world: "HatInTimeWorld"): for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + if entrance.parent_region.name == "Alpine Free Roam": + add_rule(entrance, + lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True)) if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index dd5e88abbc66..14cf13ec346d 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -253,7 +253,8 @@ def fill_slot_data(self) -> dict: else: item_name = loc.item.name - shop_item_names.setdefault(str(loc.address), item_name) + shop_item_names.setdefault(str(loc.address), + f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})") slot_data["ShopItemNames"] = shop_item_names diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 20dd18038a14..bd87cbf2c3ea 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections): entrances = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) - exits = set([connection[1] for connection in ( + exits = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) diff --git a/worlds/alttp/docs/plando_en.md b/worlds/alttp/docs/plando_en.md index af8cbfe1b039..13224cb4d54e 100644 --- a/worlds/alttp/docs/plando_en.md +++ b/worlds/alttp/docs/plando_en.md @@ -2,8 +2,8 @@ ## Configuration -1. Plando features have to be enabled first, before they can be used (opt-in). -2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml +1. All plando options are enabled by default, except for "items plando" which has to be enabled before it can be used (opt-in). +2. To enable it, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml file with a text editor. 3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value to `bosses, items, texts, connections` @@ -66,6 +66,7 @@ boss_shuffle: - ignored if only one world is generated - can be a number, to target that slot in the multiworld - can be a name, to target that player's world + - can be a list of names, to target those players' worlds - can be true, to target any other player's world - can be false, to target own world and is the default - can be null, to target a random world @@ -132,17 +133,15 @@ plando_items: ### Texts -- This module is disabled by default. - Has the options `text`, `at`, and `percentage` +- All of these options support subweights - percentage is the percentage chance for this text to be placed, can be omitted entirely for 100% - text is the text to be placed. - - can be weighted. - `\n` is a newline. - `@` is the entered player's name. - Warning: Text Mapper does not support full unicode. - [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758) - at is the location within the game to attach the text to. - - can be weighted. - [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499) #### Example @@ -162,7 +161,6 @@ and `uncle_dying_sewer`, then places the text "This is a plando. You've been war ### Connections -- This module is disabled by default. - Has the options `percentage`, `entrance`, `exit` and `direction`. - All options support subweights - percentage is the percentage chance for this to be connected, can be omitted entirely for 100% diff --git a/worlds/apsudoku/__init__.py b/worlds/apsudoku/__init__.py index c6bd02bdc262..04422ddb23c6 100644 --- a/worlds/apsudoku/__init__.py +++ b/worlds/apsudoku/__init__.py @@ -4,7 +4,7 @@ from ..AutoWorld import WebWorld, World class AP_SudokuWebWorld(WebWorld): - options_page = "games/Sudoku/info/en" + options_page = False theme = 'partyTime' setup_en = Tutorial( diff --git a/worlds/apsudoku/docs/setup_en.md b/worlds/apsudoku/docs/setup_en.md index cf2c755bd837..ef5a87e0b058 100644 --- a/worlds/apsudoku/docs/setup_en.md +++ b/worlds/apsudoku/docs/setup_en.md @@ -1,9 +1,7 @@ # APSudoku Setup Guide ## Required Software -- [APSudoku](https://github.com/EmilyV99/APSudoku) -- Windows (most tested on Win10) -- Other platforms might be able to build from source themselves; and may be included in the future. +- [APSudoku](https://github.com/APSudoku/APSudoku) ## General Concept @@ -13,25 +11,33 @@ Does not need to be added at the start of a seed, as it does not create any slot ## Installation Procedures -Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file. +Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform. ## Joining a MultiWorld Game -1. Run APSudoku.exe -2. Under the 'Archipelago' tab at the top-right: - - Enter the server url & port number +1. Run the APSudoku executable. +2. Under `Settings` → `Connection` at the top-right: + - Enter the server address and port number - Enter the name of the slot you wish to connect to - Enter the room password (optional) - Select DeathLink related settings (optional) - - Press connect -3. Go back to the 'Sudoku' tab - - Click the various '?' buttons for information on how to play / control -4. Choose puzzle difficulty -5. Try to solve the Sudoku. Click 'Check' when done. - + - Press `Connect` +4. Under the `Sudoku` tab + - Choose puzzle difficulty + - Click `Start` to generate a puzzle +5. Try to solve the Sudoku. Click `Check` when done + - A correct solution rewards you with 1 hint for a location in the world you are connected to + - An incorrect solution has no penalty, unless DeathLink is enabled (see below) + +Info: +- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`. +- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features +- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md) +- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted) +- Click the various `?` buttons for information on controls/how to play ## DeathLink Support -If 'DeathLink' is enabled when you click 'Connect': -- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting). -- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. +If `DeathLink` is enabled when you click `Connect`: +- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting). +- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. - On receiving a DeathLink from another player, your puzzle resets. diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index b110c316da48..67031710e4eb 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -199,8 +199,6 @@ def create_items(self): self.multiworld.itempool += pool - - def pre_fill(self): self.place_items_from_dict(unrandomized_dict) if self.options.thorn_shuffle == "vanilla": @@ -335,4 +333,4 @@ class BlasphemousItem(Item): class BlasphemousLocation(Location): - game: str = "Blasphemous" \ No newline at end of file + game: str = "Blasphemous" diff --git a/worlds/bumpstik/__init__.py b/worlds/bumpstik/__init__.py index fe261dc94d30..ffe9efd2de87 100644 --- a/worlds/bumpstik/__init__.py +++ b/worlds/bumpstik/__init__.py @@ -125,6 +125,6 @@ def set_rules(self): lambda state: state.has("Hazard Bumper", self.player, 25) self.multiworld.completion_condition[self.player] = \ - lambda state: state.has("Booster Bumper", self.player, 5) and \ - state.has("Treasure Bumper", self.player, 32) + lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \ + self.player) diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index e064a1c41947..9ba57b059185 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -44,15 +44,15 @@ def create_regions(self): self.multiworld.regions += [menu, board] def create_items(self): - # Generate item pool - itempool = [] + # Generate list of items + items_to_create = [] # Add the map width and height stuff - itempool += ["Map Width"] * 5 # 10 - 5 - itempool += ["Map Height"] * 5 # 10 - 5 + items_to_create += ["Map Width"] * 5 # 10 - 5 + items_to_create += ["Map Height"] * 5 # 10 - 5 # Add the map bombs - itempool += ["Map Bombs"] * 15 # 20 - 5 - # Convert itempool into real items - itempool = [self.create_item(item) for item in itempool] + items_to_create += ["Map Bombs"] * 15 # 20 - 5 + # Convert list into real items + itempool = [self.create_item(item) for item in items_to_create] self.multiworld.itempool += itempool diff --git a/worlds/dark_souls_3/Bosses.py b/worlds/dark_souls_3/Bosses.py index 008a29713202..fac7d913c338 100644 --- a/worlds/dark_souls_3/Bosses.py +++ b/worlds/dark_souls_3/Bosses.py @@ -63,6 +63,9 @@ class DS3BossInfo: DS3BossInfo("Deacons of the Deep", 3500800, locations = { "CD: Soul of the Deacons of the Deep", "CD: Small Doll - boss drop", + "CD: Archdeacon White Crown - boss room after killing boss", + "CD: Archdeacon Holy Garb - boss room after killing boss", + "CD: Archdeacon Skirt - boss room after killing boss", "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", }), DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = { diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index 19cd79a99414..044e3616f703 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -238,15 +238,6 @@ def upgrade(self, level: int) -> "DS3ItemData": ds3_code = cast(int, self.ds3_code) + level, filler = False, ) - - def __hash__(self) -> int: - return (self.name, self.ds3_code).__hash__() - - def __eq__(self, other: Any) -> bool: - if isinstance(other, self.__class__): - return self.name == other.name and self.ds3_code == other.ds3_code - else: - return False class DarkSouls3Item(Item): diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 159a870c7658..b51668539be2 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -612,9 +612,7 @@ def set_rules(self) -> None: self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows") # Define the access rules to some specific locations - if self._is_location_available("FS: Lift Chamber Key - Leonhard"): - self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", - "Lift Chamber Key") + self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", "Lift Chamber Key") self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", "Jailbreaker's Key") self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key") @@ -1252,6 +1250,9 @@ def _add_allow_useful_location_rules(self) -> None: lambda item: not item.advancement ) + # Prevent the player from prioritizing and "excluding" the same location + self.options.priority_locations.value -= allow_useful_locations + if self.options.excluded_location_behavior == "allow_useful": self.options.exclude_locations.value.clear() @@ -1292,10 +1293,10 @@ def _add_location_rule(self, location: Union[str, List[str]], rule: Union[Collec locations = location if isinstance(location, list) else [location] for location in locations: data = location_dictionary[location] - if data.dlc and not self.options.enable_dlc: return - if data.ngp and not self.options.enable_ngp: return + if data.dlc and not self.options.enable_dlc: continue + if data.ngp and not self.options.enable_ngp: continue - if not self._is_location_available(location): return + if not self._is_location_available(location): continue if isinstance(rule, str): assert item_dictionary[rule].classification == ItemClassification.progression rule = lambda state, item=rule: state.has(item, self.player) @@ -1504,16 +1505,19 @@ def fill_slot_data(self) -> Dict[str, object]: # We include all the items the game knows about so that users can manually request items # that aren't randomized, and then we _also_ include all the items that are placed in # practice `item_dictionary.values()` doesn't include upgraded or infused weapons. - all_items = { - cast(DarkSouls3Item, location.item).data + items_by_name = { + location.item.name: cast(DarkSouls3Item, location.item).data for location in self.multiworld.get_filled_locations() # item.code None is used for events, which we want to skip if location.item.code is not None and location.item.player == self.player - }.union(item_dictionary.values()) + } + for item in item_dictionary.values(): + if item.name not in items_by_name: + items_by_name[item.name] = item ap_ids_to_ds3_ids: Dict[str, int] = {} item_counts: Dict[str, int] = {} - for item in all_items: + for item in items_by_name.values(): if item.ap_code is None: continue if item.ds3_code: ap_ids_to_ds3_ids[str(item.ap_code)] = item.ds3_code if item.count != 1: item_counts[str(item.ap_code)] = item.count diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index ed90289a8baf..9755cce1c6a8 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -3,7 +3,7 @@ ## Required Software - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) -- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) +- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest) ## Optional Software @@ -11,8 +11,9 @@ ## Setting Up -First, download the client from the link above. It doesn't need to go into any particular directory; -it'll automatically locate _Dark Souls III_ in your Steam installation folder. +First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go +into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam +installation folder. Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This is the latest version, so you don't need to do any downpatching! However, if you've already @@ -35,8 +36,9 @@ randomized item and (optionally) enemy locations. You only need to do this once To run _Dark Souls III_ in Archipelago mode: -1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the - DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn. +1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain + scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu + screen. 2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that you can use to interact with the Archipelago server. @@ -52,4 +54,21 @@ To run _Dark Souls III_ in Archipelago mode: ### Where do I get a config file? The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to -configure your personal options and export them into a config file. +configure your personal options and export them into a config file. The [AP client archive] also +includes an options template. + +[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest + +### Does this work with Proton? + +The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few +things to keep in mind: + +* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install + the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under + plain WINE as well. It won't work as a Proton app! + +* To run the game itself, just run `launchmod_darksouls3.bat` under Proton. + +[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0 +[WINE]: https://www.winehq.org/ diff --git a/worlds/doom_1993/Locations.py b/worlds/doom_1993/Locations.py index 2cbb9b9d150e..90a6916cd716 100644 --- a/worlds/doom_1993/Locations.py +++ b/worlds/doom_1993/Locations.py @@ -2214,13 +2214,13 @@ class LocationDict(TypedDict, total=False): 'map': 2, 'index': 217, 'doom_type': 2006, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351367: {'name': 'Perfect Hatred (E4M2) - Exit', 'episode': 4, 'map': 2, 'index': -1, 'doom_type': -1, - 'region': "Perfect Hatred (E4M2) Blue"}, + 'region': "Perfect Hatred (E4M2) Upper"}, 351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability', 'episode': 4, 'map': 3, diff --git a/worlds/doom_1993/Regions.py b/worlds/doom_1993/Regions.py index f013bdceaf07..c32f7b470101 100644 --- a/worlds/doom_1993/Regions.py +++ b/worlds/doom_1993/Regions.py @@ -502,13 +502,12 @@ class RegionDict(TypedDict, total=False): "episode":4, "connections":[ {"target":"Perfect Hatred (E4M2) Blue","pro":False}, - {"target":"Perfect Hatred (E4M2) Yellow","pro":False}]}, + {"target":"Perfect Hatred (E4M2) Yellow","pro":False}, + {"target":"Perfect Hatred (E4M2) Upper","pro":True}]}, {"name":"Perfect Hatred (E4M2) Blue", "connects_to_hub":False, "episode":4, - "connections":[ - {"target":"Perfect Hatred (E4M2) Main","pro":False}, - {"target":"Perfect Hatred (E4M2) Cave","pro":False}]}, + "connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]}, {"name":"Perfect Hatred (E4M2) Yellow", "connects_to_hub":False, "episode":4, @@ -518,7 +517,13 @@ class RegionDict(TypedDict, total=False): {"name":"Perfect Hatred (E4M2) Cave", "connects_to_hub":False, "episode":4, - "connections":[]}, + "connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]}, + {"name":"Perfect Hatred (E4M2) Upper", + "connects_to_hub":False, + "episode":4, + "connections":[ + {"target":"Perfect Hatred (E4M2) Cave","pro":False}, + {"target":"Perfect Hatred (E4M2) Main","pro":False}]}, # Sever the Wicked (E4M3) {"name":"Sever the Wicked (E4M3) Main", diff --git a/worlds/doom_1993/Rules.py b/worlds/doom_1993/Rules.py index 4faeb4a27dbd..89b09ff9f250 100644 --- a/worlds/doom_1993/Rules.py +++ b/worlds/doom_1993/Rules.py @@ -403,9 +403,8 @@ def set_episode4_rules(player, multiworld, pro): state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or state.has("BFG9000", player, 1))) set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state: - state.has("Shotgun", player, 1) or - state.has("Chaingun", player, 1) or - state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) + (state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or + state.has("Chaingun", player, 1))) # Perfect Hatred (E4M2) set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state: diff --git a/worlds/doom_ii/Locations.py b/worlds/doom_ii/Locations.py index 3ce87b8a6662..376f19446f21 100644 --- a/worlds/doom_ii/Locations.py +++ b/worlds/doom_ii/Locations.py @@ -1470,7 +1470,7 @@ class LocationDict(TypedDict, total=False): 'map': 6, 'index': 102, 'doom_type': 2006, - 'region': "Tenements (MAP17) Main"}, + 'region': "Tenements (MAP17) Yellow"}, 361243: {'name': 'Tenements (MAP17) - Plasma gun', 'episode': 2, 'map': 6, diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index d7b3d4b1ebca..7eec71875829 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -1,5 +1,6 @@ """Outputs a Factorio Mod to facilitate integration with Archipelago""" +import dataclasses import json import os import shutil @@ -88,6 +89,8 @@ def write_contents(self, opened_zipfile: zipfile.ZipFile): def generate_mod(world: "Factorio", output_directory: str): player = world.player multiworld = world.multiworld + random = world.random + global data_final_template, locale_template, control_template, data_template, settings_template with template_load_lock: if not data_final_template: @@ -110,8 +113,6 @@ def load_template(name: str): mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" versioned_mod_name = mod_name + "_" + Utils.__version__ - random = multiworld.per_slot_randoms[player] - def flop_random(low, high, base=None): """Guarantees 50% below base and 50% above base, uniform distribution in each direction.""" if base: @@ -129,43 +130,43 @@ def flop_random(low, high, base=None): "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, "mod_name": mod_name, - "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), - "custom_technologies": multiworld.worlds[player].custom_technologies, + "allowed_science_packs": world.options.max_science_pack.get_allowed_packs(), + "custom_technologies": world.custom_technologies, "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, - "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, + "slot_name": world.player_name, "seed_name": multiworld.seed_name, "slot_player": player, - "starting_items": multiworld.starting_items[player], "recipes": recipes, + "starting_items": world.options.starting_items, "recipes": recipes, "random": random, "flop_random": flop_random, - "recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), - "recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), + "recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None), + "recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None), "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, "progressive_technology_table": {tech.name: tech.progressive for tech in progressive_technology_table.values()}, "custom_recipes": world.custom_recipes, - "max_science_pack": multiworld.max_science_pack[player].value, + "max_science_pack": world.options.max_science_pack.value, "liquids": fluids, - "goal": multiworld.goal[player].value, - "energy_link": multiworld.energy_link[player].value, + "goal": world.options.goal.value, + "energy_link": world.options.energy_link.value, "useless_technologies": useless_technologies, - "chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, + "chunk_shuffle": 0, } - for factorio_option in Options.factorio_options: + for factorio_option, factorio_option_instance in dataclasses.asdict(world.options).items(): if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]: continue - template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value + template_data[factorio_option] = factorio_option_instance.value - if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe: + if world.options.silo == Options.Silo.option_randomize_recipe: template_data["free_sample_blacklist"]["rocket-silo"] = 1 - if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe: + if world.options.satellite == Options.Satellite.option_randomize_recipe: template_data["free_sample_blacklist"]["satellite"] = 1 - template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value}) - template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value}) + template_data["free_sample_blacklist"].update({item: 1 for item in world.options.free_sample_blacklist.value}) + template_data["free_sample_blacklist"].update({item: 0 for item in world.options.free_sample_whitelist.value}) zf_path = os.path.join(output_directory, versioned_mod_name + ".zip") - mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player]) + mod = FactorioModFile(zf_path, player=player, player_name=world.player_name) if world.zip_path: with zipfile.ZipFile(world.zip_path) as zf: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 3429ebbd4251..788d1f9e1d92 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,11 +1,14 @@ from __future__ import annotations -import typing + +from dataclasses import dataclass import datetime +import typing -from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ - StartInventoryPool from schema import Schema, Optional, And, Or +from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ + StartInventoryPool, PerGameCommonOptions + # schema helpers FloatRange = lambda low, high: And(Or(int, float), lambda f: low <= f <= high) LuaBool = Or(bool, And(int, lambda n: n in (0, 1))) @@ -422,50 +425,37 @@ class EnergyLink(Toggle): display_name = "EnergyLink" -factorio_options: typing.Dict[str, type(Option)] = { - "max_science_pack": MaxSciencePack, - "goal": Goal, - "tech_tree_layout": TechTreeLayout, - "min_tech_cost": MinTechCost, - "max_tech_cost": MaxTechCost, - "tech_cost_distribution": TechCostDistribution, - "tech_cost_mix": TechCostMix, - "ramping_tech_costs": RampingTechCosts, - "silo": Silo, - "satellite": Satellite, - "free_samples": FreeSamples, - "tech_tree_information": TechTreeInformation, - "starting_items": FactorioStartItems, - "free_sample_blacklist": FactorioFreeSampleBlacklist, - "free_sample_whitelist": FactorioFreeSampleWhitelist, - "recipe_time": RecipeTime, - "recipe_ingredients": RecipeIngredients, - "recipe_ingredients_offset": RecipeIngredientsOffset, - "imported_blueprints": ImportedBlueprint, - "world_gen": FactorioWorldGen, - "progressive": Progressive, - "teleport_traps": TeleportTrapCount, - "grenade_traps": GrenadeTrapCount, - "cluster_grenade_traps": ClusterGrenadeTrapCount, - "artillery_traps": ArtilleryTrapCount, - "atomic_rocket_traps": AtomicRocketTrapCount, - "attack_traps": AttackTrapCount, - "evolution_traps": EvolutionTrapCount, - "evolution_trap_increase": EvolutionTrapIncrease, - "death_link": DeathLink, - "energy_link": EnergyLink, - "start_inventory_from_pool": StartInventoryPool, -} - -# spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else. -if datetime.datetime.today().month == 4: - - class ChunkShuffle(Toggle): - """Entrance Randomizer.""" - display_name = "Chunk Shuffle" - - - if datetime.datetime.today().day > 1: - ChunkShuffle.__doc__ += """ - 2023 April Fool's option. Shuffles chunk border transitions.""" - factorio_options["chunk_shuffle"] = ChunkShuffle +@dataclass +class FactorioOptions(PerGameCommonOptions): + max_science_pack: MaxSciencePack + goal: Goal + tech_tree_layout: TechTreeLayout + min_tech_cost: MinTechCost + max_tech_cost: MaxTechCost + tech_cost_distribution: TechCostDistribution + tech_cost_mix: TechCostMix + ramping_tech_costs: RampingTechCosts + silo: Silo + satellite: Satellite + free_samples: FreeSamples + tech_tree_information: TechTreeInformation + starting_items: FactorioStartItems + free_sample_blacklist: FactorioFreeSampleBlacklist + free_sample_whitelist: FactorioFreeSampleWhitelist + recipe_time: RecipeTime + recipe_ingredients: RecipeIngredients + recipe_ingredients_offset: RecipeIngredientsOffset + imported_blueprints: ImportedBlueprint + world_gen: FactorioWorldGen + progressive: Progressive + teleport_traps: TeleportTrapCount + grenade_traps: GrenadeTrapCount + cluster_grenade_traps: ClusterGrenadeTrapCount + artillery_traps: ArtilleryTrapCount + atomic_rocket_traps: AtomicRocketTrapCount + attack_traps: AttackTrapCount + evolution_traps: EvolutionTrapCount + evolution_trap_increase: EvolutionTrapIncrease + death_link: DeathLink + energy_link: EnergyLink + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index d40871f7fa82..2a81cc3fb004 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -19,12 +19,10 @@ def _sorter(location: "FactorioScienceLocation"): return location.complexity, location.rel_cost -def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: - world = factorio_world.multiworld - player = factorio_world.player +def get_shapes(world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} - layout = world.tech_tree_layout[player].value - locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name) + layout = world.options.tech_tree_layout.value + locations: List["FactorioScienceLocation"] = sorted(world.science_locations, key=lambda loc: loc.name) world.random.shuffle(locations) if layout == TechTreeLayout.option_single: @@ -247,5 +245,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se else: raise NotImplementedError(f"Layout {layout} is not implemented.") - factorio_world.tech_tree_layout_prerequisites = prerequisites + world.tech_tree_layout_prerequisites = prerequisites return prerequisites diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 096396c0e774..112cc49f0920 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -13,12 +13,11 @@ from . import Options factorio_tech_id = factorio_base_id = 2 ** 17 -# Factorio technologies are imported from a .json document in /data -source_folder = os.path.join(os.path.dirname(__file__), "data") pool = ThreadPoolExecutor(1) +# Factorio technologies are imported from a .json document in /data def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json")) @@ -99,7 +98,7 @@ def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: i and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) or origin.name == "rocket-silo") self.player = player - if origin.name not in world.worlds[player].special_nodes: + if origin.name not in world.special_nodes: if military_allowed: ingredients.add("military-science-pack") ingredients = list(ingredients) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 1ea2f6e4c98c..925327655a24 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -11,7 +11,7 @@ from worlds.generic import Rules from .Locations import location_pools, location_table from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution +from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ @@ -89,18 +89,21 @@ class Factorio(World): advancement_technologies: typing.Set[str] web = FactorioWeb() + options_dataclass = FactorioOptions + options: FactorioOptions item_name_to_id = all_items location_name_to_id = location_table item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - required_client_version = (0, 4, 2) + required_client_version = (0, 5, 0) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() 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] @@ -116,35 +119,32 @@ def __init__(self, world, player: int): def generate_early(self) -> None: # if max < min, then swap max and min - if self.multiworld.max_tech_cost[self.player] < self.multiworld.min_tech_cost[self.player]: - self.multiworld.min_tech_cost[self.player].value, self.multiworld.max_tech_cost[self.player].value = \ - self.multiworld.max_tech_cost[self.player].value, self.multiworld.min_tech_cost[self.player].value - self.tech_mix = self.multiworld.tech_cost_mix[self.player] - self.skip_silo = self.multiworld.silo[self.player].value == Silo.option_spawn + if self.options.max_tech_cost < self.options.min_tech_cost: + self.options.min_tech_cost.value, self.options.max_tech_cost.value = \ + self.options.max_tech_cost.value, self.options.min_tech_cost.value + self.tech_mix = self.options.tech_cost_mix.value + self.skip_silo = self.options.silo.value == Silo.option_spawn 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) + random = self.random nauvis = Region("Nauvis", player, self.multiworld) location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ - self.multiworld.evolution_traps[player] + \ - self.multiworld.attack_traps[player] + \ - self.multiworld.teleport_traps[player] + \ - self.multiworld.grenade_traps[player] + \ - self.multiworld.cluster_grenade_traps[player] + \ - self.multiworld.atomic_rocket_traps[player] + \ - self.multiworld.artillery_traps[player] + self.options.evolution_traps + \ + self.options.attack_traps + \ + self.options.teleport_traps + \ + self.options.grenade_traps + \ + self.options.cluster_grenade_traps + \ + self.options.atomic_rocket_traps + \ + self.options.artillery_traps location_pool = [] - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): location_pool.extend(location_pools[pack]) try: - location_names = self.multiworld.random.sample(location_pool, location_count) + location_names = random.sample(location_pool, location_count) except ValueError as e: # should be "ValueError: Sample larger than population or is negative" raise Exception("Too many traps for too few locations. Either decrease the trap count, " @@ -152,9 +152,9 @@ def create_regions(self): self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) for loc_name in location_names] - distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] - min_cost = self.multiworld.min_tech_cost[self.player] - max_cost = self.multiworld.max_tech_cost[self.player] + distribution: TechCostDistribution = self.options.tech_cost_distribution + min_cost = self.options.min_tech_cost.value + max_cost = self.options.max_tech_cost.value if distribution == distribution.option_even: rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations) else: @@ -163,7 +163,7 @@ def create_regions(self): distribution.option_high: max_cost}[distribution.value] rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations) rand_values = sorted(rand_values) - if self.multiworld.ramping_tech_costs[self.player]: + if self.options.ramping_tech_costs: def sorter(loc: FactorioScienceLocation): return loc.complexity, loc.rel_cost else: @@ -178,34 +178,32 @@ def sorter(loc: FactorioScienceLocation): event = FactorioItem("Victory", ItemClassification.progression, None, player) location.place_locked_item(event) - for ingredient in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for ingredient in sorted(self.options.max_science_pack.get_allowed_packs()): location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis) nauvis.locations.append(location) 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 self.custom_technologies = self.set_custom_technologies() self.set_custom_recipes() traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") for trap_name in traps: self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in - range(getattr(self.multiworld, - f"{trap_name.lower().replace(' ', '_')}_traps")[player])) + range(getattr(self.options, + f"{trap_name.lower().replace(' ', '_')}_traps"))) - want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. - want_progressives(self.multiworld.random)) + want_progressives = collections.defaultdict(lambda: self.options.progressive. + want_progressives(self.random)) cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name) special_index = {"automation": 0, "logistics": 1, "rocket-silo": -1} loc: FactorioScienceLocation - if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full: + if self.options.tech_tree_information == TechTreeInformation.option_full: # mark all locations as pre-hinted for loc in self.science_locations: loc.revealed = True @@ -232,14 +230,13 @@ def create_items(self) -> None: loc.revealed = True def set_rules(self): - world = self.multiworld player = self.player shapes = get_shapes(self) - for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): - location = world.get_location(f"Automate {ingredient}", player) + for ingredient in self.options.max_science_pack.get_allowed_packs(): + location = self.get_location(f"Automate {ingredient}") - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: custom_recipe = self.custom_recipes[ingredient] location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ @@ -260,30 +257,30 @@ def set_rules(self): prerequisites: all(state.can_reach(loc) for loc in locations)) silo_recipe = None - if self.multiworld.silo[self.player] == Silo.option_spawn: + if self.options.silo == Silo.option_spawn: silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ else next(iter(all_product_sources.get("rocket-silo"))) part_recipe = self.custom_recipes["rocket-part"] satellite_recipe = None - if self.multiworld.goal[self.player] == Goal.option_satellite: + if self.options.goal == Goal.option_satellite: satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ else next(iter(all_product_sources.get("satellite"))) victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) - if self.multiworld.silo[self.player] != Silo.option_spawn: + if self.options.silo != Silo.option_spawn: victory_tech_names.add("rocket-silo") - world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in - victory_tech_names) + self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) + for technology in + victory_tech_names) - world.completion_condition[player] = lambda state: state.has('Victory', player) + self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) def generate_basic(self): - map_basic_settings = self.multiworld.world_gen[self.player].value["basic"] + map_basic_settings = self.options.world_gen.value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 # 32 bit uint - map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1) + map_basic_settings["seed"] = self.random.randint(0, 2 ** 32 - 1) - start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value + start_location_hints: typing.Set[str] = self.options.start_location_hints.value for loc in self.science_locations: # show start_location_hints ingame @@ -307,8 +304,6 @@ def collect_item(self, state, item, remove=False): return super(Factorio, self).collect_item(state, item, remove) - option_definitions = factorio_options - @classmethod def stage_write_spoiler(cls, world, spoiler_handle): factorio_players = world.get_game_players(cls.game) @@ -348,7 +343,7 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: # have to first sort for determinism, while filtering out non-stacking items pool: typing.List[str] = sorted(pool & valid_ingredients) # then sort with random data to shuffle - self.multiworld.random.shuffle(pool) + self.random.shuffle(pool) target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor) target_energy = original.total_energy * factor target_num_ingredients = len(original.ingredients) + ingredients_offset @@ -392,7 +387,7 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: if min_num > max_num: fallback_pool.append(ingredient) continue # can't use that ingredient - num = self.multiworld.random.randint(min_num, max_num) + num = self.random.randint(min_num, max_num) new_ingredients[ingredient] = num remaining_raw -= num * ingredient_raw remaining_energy -= num * ingredient_energy @@ -436,66 +431,66 @@ def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: def set_custom_technologies(self): custom_technologies = {} - allowed_packs = self.multiworld.max_science_pack[self.player].get_allowed_packs() + allowed_packs = self.options.max_science_pack.get_allowed_packs() for technology_name, technology in base_technology_table.items(): - custom_technologies[technology_name] = technology.get_custom(self.multiworld, allowed_packs, self.player) + custom_technologies[technology_name] = technology.get_custom(self, allowed_packs, self.player) return custom_technologies def set_custom_recipes(self): - ingredients_offset = self.multiworld.recipe_ingredients_offset[self.player] + ingredients_offset = self.options.recipe_ingredients_offset original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() - valid_pool = sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_max_pack()] & valid_ingredients) - self.multiworld.random.shuffle(valid_pool) + valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients) + self.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, {valid_pool[x]: 10 for x in range(3 + ingredients_offset)}, original_rocket_part.products, original_rocket_part.energy)} - if self.multiworld.recipe_ingredients[self.player]: + if self.options.recipe_ingredients: valid_pool = [] - for pack in self.multiworld.max_science_pack[self.player].get_ordered_science_packs(): + for pack in self.options.max_science_pack.get_ordered_science_packs(): valid_pool += sorted(science_pack_pools[pack]) - self.multiworld.random.shuffle(valid_pool) + self.random.shuffle(valid_pool) if pack in recipes: # skips over space science pack new_recipe = self.make_quick_recipe(recipes[pack], valid_pool, ingredients_offset= - ingredients_offset) + ingredients_offset.value) self.custom_recipes[pack] = new_recipe - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe \ - or self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe \ + or self.options.satellite.value == Satellite.option_randomize_recipe: valid_pool = set() - for pack in sorted(self.multiworld.max_science_pack[self.player].get_allowed_packs()): + for pack in sorted(self.options.max_science_pack.get_allowed_packs()): valid_pool |= science_pack_pools[pack] - if self.multiworld.silo[self.player].value == Silo.option_randomize_recipe: + if self.options.silo.value == Silo.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["rocket-silo"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["rocket-silo"] = new_recipe - if self.multiworld.satellite[self.player].value == Satellite.option_randomize_recipe: + if self.options.satellite.value == Satellite.option_randomize_recipe: new_recipe = self.make_balanced_recipe( recipes["satellite"], valid_pool, - factor=(self.multiworld.max_science_pack[self.player].value + 1) / 7, - ingredients_offset=ingredients_offset) + factor=(self.options.max_science_pack.value + 1) / 7, + ingredients_offset=ingredients_offset.value) self.custom_recipes["satellite"] = new_recipe bridge = "ap-energy-bridge" new_recipe = self.make_quick_recipe( Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1, "replace_4": 1, "replace_5": 1, "replace_6": 1}, {bridge: 1}, 10), - sorted(science_pack_pools[self.multiworld.max_science_pack[self.player].get_ordered_science_packs()[0]]), - ingredients_offset=ingredients_offset) + sorted(science_pack_pools[self.options.max_science_pack.get_ordered_science_packs()[0]]), + ingredients_offset=ingredients_offset.value) for ingredient_name in new_recipe.ingredients: - new_recipe.ingredients[ingredient_name] = self.multiworld.random.randint(50, 500) + new_recipe.ingredients[ingredient_name] = self.random.randint(50, 500) self.custom_recipes[bridge] = new_recipe - needed_recipes = self.multiworld.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"} - if self.multiworld.silo[self.player] != Silo.option_spawn: + needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} + if self.options.silo != Silo.option_spawn: needed_recipes |= {"rocket-silo"} - if self.multiworld.goal[self.player].value == Goal.option_satellite: + if self.options.goal.value == Goal.option_satellite: needed_recipes |= {"satellite"} for recipe in needed_recipes: @@ -545,7 +540,8 @@ def __init__(self, player: int, name: str, address: int, parent: Region): self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} for complexity in range(self.complexity): - if parent.multiworld.tech_cost_mix[self.player] > parent.multiworld.random.randint(0, 99): + if (parent.multiworld.worlds[self.player].options.tech_cost_mix > + parent.multiworld.worlds[self.player].random.randint(0, 99)): self.ingredients[Factorio.ordered_science_packs[complexity]] = 1 @property diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index 161b1e465b33..1980e81cbcc4 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -22,9 +22,9 @@ enabled (opt-in). * You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like: ```yaml - requires: - version: current.version.number - plando: bosses, items, texts, connections +requires: + version: current.version.number + plando: bosses, items, texts, connections ``` ## Item Plando @@ -74,77 +74,77 @@ A list of all available items and locations can be found in the [website's datap ### Examples ```yaml -plando_items: - # example block 1 - Timespinner - - item: - Empire Orb: 1 - Radiant Orb: 1 - location: Starter Chest 1 - from_pool: true - world: true - percentage: 50 - - # example block 2 - Ocarina of Time - - items: - Kokiri Sword: 1 - Biggoron Sword: 1 - Bow: 1 - Magic Meter: 1 - Progressive Strength Upgrade: 3 - Progressive Hookshot: 2 - locations: - - Deku Tree Slingshot Chest - - Dodongos Cavern Bomb Bag Chest - - Jabu Jabus Belly Boomerang Chest - - Bottom of the Well Lens of Truth Chest - - Forest Temple Bow Chest - - Fire Temple Megaton Hammer Chest - - Water Temple Longshot Chest - - Shadow Temple Hover Boots Chest - - Spirit Temple Silver Gauntlets Chest - world: false - - # example block 3 - Slay the Spire - - items: - Boss Relic: 3 - locations: - - Boss Relic 1 - - Boss Relic 2 - - Boss Relic 3 - - # example block 4 - Factorio - - items: - progressive-electric-energy-distribution: 2 - electric-energy-accumulators: 1 - progressive-turret: 2 - locations: - - military - - gun-turret - - logistic-science-pack - - steel-processing - percentage: 80 - force: true - -# example block 5 - Secret of Evermore - - items: - Levitate: 1 - Revealer: 1 - Energize: 1 - locations: - - Master Sword Pedestal - - Boss Relic 1 - world: true - count: 2 - -# example block 6 - A Link to the Past - - items: - Progressive Sword: 4 - world: - - BobsSlaytheSpire - - BobsRogueLegacy - count: - min: 1 - max: 4 + plando_items: + # example block 1 - Timespinner + - item: + Empire Orb: 1 + Radiant Orb: 1 + location: Starter Chest 1 + from_pool: true + world: true + percentage: 50 + + # example block 2 - Ocarina of Time + - items: + Kokiri Sword: 1 + Biggoron Sword: 1 + Bow: 1 + Magic Meter: 1 + Progressive Strength Upgrade: 3 + Progressive Hookshot: 2 + locations: + - Deku Tree Slingshot Chest + - Dodongos Cavern Bomb Bag Chest + - Jabu Jabus Belly Boomerang Chest + - Bottom of the Well Lens of Truth Chest + - Forest Temple Bow Chest + - Fire Temple Megaton Hammer Chest + - Water Temple Longshot Chest + - Shadow Temple Hover Boots Chest + - Spirit Temple Silver Gauntlets Chest + world: false + + # example block 3 - Slay the Spire + - items: + Boss Relic: 3 + locations: + - Boss Relic 1 + - Boss Relic 2 + - Boss Relic 3 + + # example block 4 - Factorio + - items: + progressive-electric-energy-distribution: 2 + electric-energy-accumulators: 1 + progressive-turret: 2 + locations: + - military + - gun-turret + - logistic-science-pack + - steel-processing + percentage: 80 + force: true + + # example block 5 - Secret of Evermore + - items: + Levitate: 1 + Revealer: 1 + Energize: 1 + locations: + - Master Sword Pedestal + - Boss Relic 1 + world: true + count: 2 + + # example block 6 - A Link to the Past + - items: + Progressive Sword: 4 + world: + - BobsSlaytheSpire + - BobsRogueLegacy + count: + min: 1 + max: 4 ``` 1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another player's Starter Chest 1 and removes the chosen item from the item pool. @@ -221,25 +221,25 @@ its [plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en#connections). ### Examples ```yaml -plando_connections: - # example block 1 - A Link to the Past - - entrance: Cave Shop (Lake Hylia) - exit: Cave 45 - direction: entrance - - entrance: Cave 45 - exit: Cave Shop (Lake Hylia) - direction: entrance - - entrance: Agahnims Tower - exit: Old Man Cave Exit (West) - direction: exit - - # example block 2 - Minecraft - - entrance: Overworld Structure 1 - exit: Nether Fortress - direction: both - - entrance: Overworld Structure 2 - exit: Village - direction: both + plando_connections: + # example block 1 - A Link to the Past + - entrance: Cave Shop (Lake Hylia) + exit: Cave 45 + direction: entrance + - entrance: Cave 45 + exit: Cave Shop (Lake Hylia) + direction: entrance + - entrance: Agahnims Tower + exit: Old Man Cave Exit (West) + direction: exit + + # example block 2 - Minecraft + - entrance: Overworld Structure 1 + exit: Nether Fortress + direction: both + - entrance: Overworld Structure 2 + exit: Village + direction: both ``` 1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index cbb909606127..9ec77e6bf0cd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -21,6 +21,16 @@ from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld +from settings import Group, Bool + + +class HollowKnightSettings(Group): + class DisableMapModSpoilers(Bool): + """Disallows the APMapMod from showing spoiler placements.""" + + disable_spoilers: typing.Union[DisableMapModSpoilers, bool] = False + + path_of_pain_locations = { "Soul_Totem-Path_of_Pain_Below_Thornskip", "Lore_Tablet-Path_of_Pain_Entrance", @@ -124,14 +134,25 @@ class HKWeb(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Mod Setup and Use Guide", "A guide to playing Hollow Knight with Archipelago.", "English", "setup_en.md", "setup/en", ["Ijwu"] - )] + ) + + setup_pt_br = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Português Brasileiro", + "setup_pt_br.md", + "setup/pt_br", + ["JoaoVictor-FA"] + ) + + tutorials = [setup_en, setup_pt_br] bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title=" @@ -145,6 +166,7 @@ class HKWorld(World): game: str = "Hollow Knight" options_dataclass = HKOptions options: HKOptions + settings: typing.ClassVar[HollowKnightSettings] web = HKWeb() @@ -512,26 +534,16 @@ def fill_slot_data(self): for option_name in hollow_knight_options: option = getattr(self.options, option_name) try: + # exclude more complex types - we only care about int, bool, enum for player options; the client + # can get them back to the necessary type. optionvalue = int(option.value) - except TypeError: - pass # C# side is currently typed as dict[str, int], drop what doesn't fit - else: options[option_name] = optionvalue + except TypeError: + pass # 32 bit int slot_data["seed"] = self.random.randint(-2147483647, 2147483646) - # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.options.CostSanity: - for shop, terms in shop_cost_types.items(): - unit = cost_terms[next(iter(terms))].option - if unit == "Geo": - continue - slot_data[f"{unit}_costs"] = { - loc.name: next(iter(loc.costs.values())) - for loc in self.created_multi_locations[shop] - } - # HKAP 0.1.0 and later cost data. location_costs = {} for region in self.multiworld.get_regions(self.player): @@ -544,6 +556,8 @@ def fill_slot_data(self): slot_data["grub_count"] = self.grub_count + slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race + return slot_data def create_item(self, name: str) -> HKItem: @@ -601,11 +615,11 @@ def collect(self, state, item: HKItem) -> bool: 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: diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index c046785038d8..21cdcb68b3a9 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -15,7 +15,7 @@ ### What to do if Lumafly fails to find your installation directory 1. Find the directory manually. * Xbox Game Pass: - 1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar. + 1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar. 2. Click the three points then click "Manage". 3. Go to the "Files" tab and select "Browse...". 4. Click "Hollow Knight", then "Content", then click the path bar and copy it. diff --git a/worlds/hk/docs/setup_pt_br.md b/worlds/hk/docs/setup_pt_br.md new file mode 100644 index 000000000000..9ae1ea89d566 --- /dev/null +++ b/worlds/hk/docs/setup_pt_br.md @@ -0,0 +1,52 @@ +# Guia de configuração para Hollow Knight no Archipelago + +## Programas obrigatórios +* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/). +* Uma cópia legal de Hollow Knight. + * Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas. + * Windows, Mac, e Linux (incluindo Steam Deck) são suportados. + +## Instalando o mod Archipelago Mod usando Lumafly +1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight. +2. Clique em "Install (instalar)" perto da opção "Archipelago" mod. + * Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo. +3. Abra o jogo, tudo preparado! + +### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação +1. Encontre a pasta manualmente. + * Xbox Game Pass: + 1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda. + 2. Clique nos 3 pontos depois clique gerenciar. + 3. Vá nos arquivos e selecione procurar. + 4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie. + * Steam: + 1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está. + . Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço. + * Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight` + * Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight` + * Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app` +2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou. + +## Configurando seu arquivo YAML +### O que é um YAML e por que eu preciso de um? +Um arquivo YAML é a forma que você informa suas configurações do jogador para o Archipelago. +Olhe o [guia de configuração básica de um multiworld](/tutorial/Archipelago/setup/en) aqui no site do Archipelago para aprender mais. + +### Onde eu consigo o YAML? +Você pode usar a [página de configurações do jogador para Hollow Knight](/games/Hollow%20Knight/player-options) aqui no site do Archipelago +para gerar o YAML usando a interface gráfica. + +### Entrando numa partida de Archipelago no Hollow Knight +1. Começe o jogo depois de instalar todos os mods necessários. +2. Crie um **novo jogo salvo.** +3. Selecione o modo de jogo **Archipelago** do menu de seleção. +4. Coloque as configurações corretas do seu servidor Archipelago. +5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens. +6. O jogo vai te colocar imediatamente numa partida randomizada. + * Se você está esperando uma contagem então espere ele cair antes de apertar começar. + * Ou clique em começar e pause o jogo enquanto estiver nele. + +## Dicas e outros comandos +Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no +[Guia de comandos](/tutorial/Archipelago/commands/en). Você pode usar o cliente de texto do Archipelago para isso, +que está incluido na ultima versão do [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest). diff --git a/worlds/kdl3/Locations.py b/worlds/kdl3/Locations.py deleted file mode 100644 index 4d039a13497c..000000000000 --- a/worlds/kdl3/Locations.py +++ /dev/null @@ -1,940 +0,0 @@ -import typing -from BaseClasses import Location, Region -from .Names import LocationName - -if typing.TYPE_CHECKING: - from .Room import KDL3Room - - -class KDL3Location(Location): - game: str = "Kirby's Dream Land 3" - room: typing.Optional["KDL3Room"] = None - - def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]): - super().__init__(player, name, address, parent) - if not address: - self.show_in_spoiler = False - - -stage_locations = { - 0x770001: LocationName.grass_land_1, - 0x770002: LocationName.grass_land_2, - 0x770003: LocationName.grass_land_3, - 0x770004: LocationName.grass_land_4, - 0x770005: LocationName.grass_land_5, - 0x770006: LocationName.grass_land_6, - 0x770007: LocationName.ripple_field_1, - 0x770008: LocationName.ripple_field_2, - 0x770009: LocationName.ripple_field_3, - 0x77000A: LocationName.ripple_field_4, - 0x77000B: LocationName.ripple_field_5, - 0x77000C: LocationName.ripple_field_6, - 0x77000D: LocationName.sand_canyon_1, - 0x77000E: LocationName.sand_canyon_2, - 0x77000F: LocationName.sand_canyon_3, - 0x770010: LocationName.sand_canyon_4, - 0x770011: LocationName.sand_canyon_5, - 0x770012: LocationName.sand_canyon_6, - 0x770013: LocationName.cloudy_park_1, - 0x770014: LocationName.cloudy_park_2, - 0x770015: LocationName.cloudy_park_3, - 0x770016: LocationName.cloudy_park_4, - 0x770017: LocationName.cloudy_park_5, - 0x770018: LocationName.cloudy_park_6, - 0x770019: LocationName.iceberg_1, - 0x77001A: LocationName.iceberg_2, - 0x77001B: LocationName.iceberg_3, - 0x77001C: LocationName.iceberg_4, - 0x77001D: LocationName.iceberg_5, - 0x77001E: LocationName.iceberg_6, -} - -heart_star_locations = { - 0x770101: LocationName.grass_land_tulip, - 0x770102: LocationName.grass_land_muchi, - 0x770103: LocationName.grass_land_pitcherman, - 0x770104: LocationName.grass_land_chao, - 0x770105: LocationName.grass_land_mine, - 0x770106: LocationName.grass_land_pierre, - 0x770107: LocationName.ripple_field_kamuribana, - 0x770108: LocationName.ripple_field_bakasa, - 0x770109: LocationName.ripple_field_elieel, - 0x77010A: LocationName.ripple_field_toad, - 0x77010B: LocationName.ripple_field_mama_pitch, - 0x77010C: LocationName.ripple_field_hb002, - 0x77010D: LocationName.sand_canyon_mushrooms, - 0x77010E: LocationName.sand_canyon_auntie, - 0x77010F: LocationName.sand_canyon_caramello, - 0x770110: LocationName.sand_canyon_hikari, - 0x770111: LocationName.sand_canyon_nyupun, - 0x770112: LocationName.sand_canyon_rob, - 0x770113: LocationName.cloudy_park_hibanamodoki, - 0x770114: LocationName.cloudy_park_piyokeko, - 0x770115: LocationName.cloudy_park_mrball, - 0x770116: LocationName.cloudy_park_mikarin, - 0x770117: LocationName.cloudy_park_pick, - 0x770118: LocationName.cloudy_park_hb007, - 0x770119: LocationName.iceberg_kogoesou, - 0x77011A: LocationName.iceberg_samus, - 0x77011B: LocationName.iceberg_kawasaki, - 0x77011C: LocationName.iceberg_name, - 0x77011D: LocationName.iceberg_shiro, - 0x77011E: LocationName.iceberg_angel, -} - -boss_locations = { - 0x770200: LocationName.grass_land_whispy, - 0x770201: LocationName.ripple_field_acro, - 0x770202: LocationName.sand_canyon_poncon, - 0x770203: LocationName.cloudy_park_ado, - 0x770204: LocationName.iceberg_dedede, -} - -consumable_locations = { - 0x770300: LocationName.grass_land_1_u1, - 0x770301: LocationName.grass_land_1_m1, - 0x770302: LocationName.grass_land_2_u1, - 0x770303: LocationName.grass_land_3_u1, - 0x770304: LocationName.grass_land_3_m1, - 0x770305: LocationName.grass_land_4_m1, - 0x770306: LocationName.grass_land_4_u1, - 0x770307: LocationName.grass_land_4_m2, - 0x770308: LocationName.grass_land_4_m3, - 0x770309: LocationName.grass_land_6_u1, - 0x77030A: LocationName.grass_land_6_u2, - 0x77030B: LocationName.ripple_field_2_u1, - 0x77030C: LocationName.ripple_field_2_m1, - 0x77030D: LocationName.ripple_field_3_m1, - 0x77030E: LocationName.ripple_field_3_u1, - 0x77030F: LocationName.ripple_field_4_m2, - 0x770310: LocationName.ripple_field_4_u1, - 0x770311: LocationName.ripple_field_4_m1, - 0x770312: LocationName.ripple_field_5_u1, - 0x770313: LocationName.ripple_field_5_m2, - 0x770314: LocationName.ripple_field_5_m1, - 0x770315: LocationName.sand_canyon_1_u1, - 0x770316: LocationName.sand_canyon_2_u1, - 0x770317: LocationName.sand_canyon_2_m1, - 0x770318: LocationName.sand_canyon_4_m1, - 0x770319: LocationName.sand_canyon_4_u1, - 0x77031A: LocationName.sand_canyon_4_m2, - 0x77031B: LocationName.sand_canyon_5_u1, - 0x77031C: LocationName.sand_canyon_5_u3, - 0x77031D: LocationName.sand_canyon_5_m1, - 0x77031E: LocationName.sand_canyon_5_u4, - 0x77031F: LocationName.sand_canyon_5_u2, - 0x770320: LocationName.cloudy_park_1_m1, - 0x770321: LocationName.cloudy_park_1_u1, - 0x770322: LocationName.cloudy_park_4_u1, - 0x770323: LocationName.cloudy_park_4_m1, - 0x770324: LocationName.cloudy_park_5_m1, - 0x770325: LocationName.cloudy_park_6_u1, - 0x770326: LocationName.iceberg_3_m1, - 0x770327: LocationName.iceberg_5_u1, - 0x770328: LocationName.iceberg_5_u2, - 0x770329: LocationName.iceberg_5_u3, - 0x77032A: LocationName.iceberg_6_m1, - 0x77032B: LocationName.iceberg_6_u1, -} - -level_consumables = { - 1: [0, 1], - 2: [2], - 3: [3, 4], - 4: [5, 6, 7, 8], - 6: [9, 10], - 8: [11, 12], - 9: [13, 14], - 10: [15, 16, 17], - 11: [18, 19, 20], - 13: [21], - 14: [22, 23], - 16: [24, 25, 26], - 17: [27, 28, 29, 30, 31], - 19: [32, 33], - 22: [34, 35], - 23: [36], - 24: [37], - 27: [38], - 29: [39, 40, 41], - 30: [42, 43], -} - -star_locations = { - 0x770401: LocationName.grass_land_1_s1, - 0x770402: LocationName.grass_land_1_s2, - 0x770403: LocationName.grass_land_1_s3, - 0x770404: LocationName.grass_land_1_s4, - 0x770405: LocationName.grass_land_1_s5, - 0x770406: LocationName.grass_land_1_s6, - 0x770407: LocationName.grass_land_1_s7, - 0x770408: LocationName.grass_land_1_s8, - 0x770409: LocationName.grass_land_1_s9, - 0x77040a: LocationName.grass_land_1_s10, - 0x77040b: LocationName.grass_land_1_s11, - 0x77040c: LocationName.grass_land_1_s12, - 0x77040d: LocationName.grass_land_1_s13, - 0x77040e: LocationName.grass_land_1_s14, - 0x77040f: LocationName.grass_land_1_s15, - 0x770410: LocationName.grass_land_1_s16, - 0x770411: LocationName.grass_land_1_s17, - 0x770412: LocationName.grass_land_1_s18, - 0x770413: LocationName.grass_land_1_s19, - 0x770414: LocationName.grass_land_1_s20, - 0x770415: LocationName.grass_land_1_s21, - 0x770416: LocationName.grass_land_1_s22, - 0x770417: LocationName.grass_land_1_s23, - 0x770418: LocationName.grass_land_2_s1, - 0x770419: LocationName.grass_land_2_s2, - 0x77041a: LocationName.grass_land_2_s3, - 0x77041b: LocationName.grass_land_2_s4, - 0x77041c: LocationName.grass_land_2_s5, - 0x77041d: LocationName.grass_land_2_s6, - 0x77041e: LocationName.grass_land_2_s7, - 0x77041f: LocationName.grass_land_2_s8, - 0x770420: LocationName.grass_land_2_s9, - 0x770421: LocationName.grass_land_2_s10, - 0x770422: LocationName.grass_land_2_s11, - 0x770423: LocationName.grass_land_2_s12, - 0x770424: LocationName.grass_land_2_s13, - 0x770425: LocationName.grass_land_2_s14, - 0x770426: LocationName.grass_land_2_s15, - 0x770427: LocationName.grass_land_2_s16, - 0x770428: LocationName.grass_land_2_s17, - 0x770429: LocationName.grass_land_2_s18, - 0x77042a: LocationName.grass_land_2_s19, - 0x77042b: LocationName.grass_land_2_s20, - 0x77042c: LocationName.grass_land_2_s21, - 0x77042d: LocationName.grass_land_3_s1, - 0x77042e: LocationName.grass_land_3_s2, - 0x77042f: LocationName.grass_land_3_s3, - 0x770430: LocationName.grass_land_3_s4, - 0x770431: LocationName.grass_land_3_s5, - 0x770432: LocationName.grass_land_3_s6, - 0x770433: LocationName.grass_land_3_s7, - 0x770434: LocationName.grass_land_3_s8, - 0x770435: LocationName.grass_land_3_s9, - 0x770436: LocationName.grass_land_3_s10, - 0x770437: LocationName.grass_land_3_s11, - 0x770438: LocationName.grass_land_3_s12, - 0x770439: LocationName.grass_land_3_s13, - 0x77043a: LocationName.grass_land_3_s14, - 0x77043b: LocationName.grass_land_3_s15, - 0x77043c: LocationName.grass_land_3_s16, - 0x77043d: LocationName.grass_land_3_s17, - 0x77043e: LocationName.grass_land_3_s18, - 0x77043f: LocationName.grass_land_3_s19, - 0x770440: LocationName.grass_land_3_s20, - 0x770441: LocationName.grass_land_3_s21, - 0x770442: LocationName.grass_land_3_s22, - 0x770443: LocationName.grass_land_3_s23, - 0x770444: LocationName.grass_land_3_s24, - 0x770445: LocationName.grass_land_3_s25, - 0x770446: LocationName.grass_land_3_s26, - 0x770447: LocationName.grass_land_3_s27, - 0x770448: LocationName.grass_land_3_s28, - 0x770449: LocationName.grass_land_3_s29, - 0x77044a: LocationName.grass_land_3_s30, - 0x77044b: LocationName.grass_land_3_s31, - 0x77044c: LocationName.grass_land_4_s1, - 0x77044d: LocationName.grass_land_4_s2, - 0x77044e: LocationName.grass_land_4_s3, - 0x77044f: LocationName.grass_land_4_s4, - 0x770450: LocationName.grass_land_4_s5, - 0x770451: LocationName.grass_land_4_s6, - 0x770452: LocationName.grass_land_4_s7, - 0x770453: LocationName.grass_land_4_s8, - 0x770454: LocationName.grass_land_4_s9, - 0x770455: LocationName.grass_land_4_s10, - 0x770456: LocationName.grass_land_4_s11, - 0x770457: LocationName.grass_land_4_s12, - 0x770458: LocationName.grass_land_4_s13, - 0x770459: LocationName.grass_land_4_s14, - 0x77045a: LocationName.grass_land_4_s15, - 0x77045b: LocationName.grass_land_4_s16, - 0x77045c: LocationName.grass_land_4_s17, - 0x77045d: LocationName.grass_land_4_s18, - 0x77045e: LocationName.grass_land_4_s19, - 0x77045f: LocationName.grass_land_4_s20, - 0x770460: LocationName.grass_land_4_s21, - 0x770461: LocationName.grass_land_4_s22, - 0x770462: LocationName.grass_land_4_s23, - 0x770463: LocationName.grass_land_4_s24, - 0x770464: LocationName.grass_land_4_s25, - 0x770465: LocationName.grass_land_4_s26, - 0x770466: LocationName.grass_land_4_s27, - 0x770467: LocationName.grass_land_4_s28, - 0x770468: LocationName.grass_land_4_s29, - 0x770469: LocationName.grass_land_4_s30, - 0x77046a: LocationName.grass_land_4_s31, - 0x77046b: LocationName.grass_land_4_s32, - 0x77046c: LocationName.grass_land_4_s33, - 0x77046d: LocationName.grass_land_4_s34, - 0x77046e: LocationName.grass_land_4_s35, - 0x77046f: LocationName.grass_land_4_s36, - 0x770470: LocationName.grass_land_4_s37, - 0x770471: LocationName.grass_land_5_s1, - 0x770472: LocationName.grass_land_5_s2, - 0x770473: LocationName.grass_land_5_s3, - 0x770474: LocationName.grass_land_5_s4, - 0x770475: LocationName.grass_land_5_s5, - 0x770476: LocationName.grass_land_5_s6, - 0x770477: LocationName.grass_land_5_s7, - 0x770478: LocationName.grass_land_5_s8, - 0x770479: LocationName.grass_land_5_s9, - 0x77047a: LocationName.grass_land_5_s10, - 0x77047b: LocationName.grass_land_5_s11, - 0x77047c: LocationName.grass_land_5_s12, - 0x77047d: LocationName.grass_land_5_s13, - 0x77047e: LocationName.grass_land_5_s14, - 0x77047f: LocationName.grass_land_5_s15, - 0x770480: LocationName.grass_land_5_s16, - 0x770481: LocationName.grass_land_5_s17, - 0x770482: LocationName.grass_land_5_s18, - 0x770483: LocationName.grass_land_5_s19, - 0x770484: LocationName.grass_land_5_s20, - 0x770485: LocationName.grass_land_5_s21, - 0x770486: LocationName.grass_land_5_s22, - 0x770487: LocationName.grass_land_5_s23, - 0x770488: LocationName.grass_land_5_s24, - 0x770489: LocationName.grass_land_5_s25, - 0x77048a: LocationName.grass_land_5_s26, - 0x77048b: LocationName.grass_land_5_s27, - 0x77048c: LocationName.grass_land_5_s28, - 0x77048d: LocationName.grass_land_5_s29, - 0x77048e: LocationName.grass_land_6_s1, - 0x77048f: LocationName.grass_land_6_s2, - 0x770490: LocationName.grass_land_6_s3, - 0x770491: LocationName.grass_land_6_s4, - 0x770492: LocationName.grass_land_6_s5, - 0x770493: LocationName.grass_land_6_s6, - 0x770494: LocationName.grass_land_6_s7, - 0x770495: LocationName.grass_land_6_s8, - 0x770496: LocationName.grass_land_6_s9, - 0x770497: LocationName.grass_land_6_s10, - 0x770498: LocationName.grass_land_6_s11, - 0x770499: LocationName.grass_land_6_s12, - 0x77049a: LocationName.grass_land_6_s13, - 0x77049b: LocationName.grass_land_6_s14, - 0x77049c: LocationName.grass_land_6_s15, - 0x77049d: LocationName.grass_land_6_s16, - 0x77049e: LocationName.grass_land_6_s17, - 0x77049f: LocationName.grass_land_6_s18, - 0x7704a0: LocationName.grass_land_6_s19, - 0x7704a1: LocationName.grass_land_6_s20, - 0x7704a2: LocationName.grass_land_6_s21, - 0x7704a3: LocationName.grass_land_6_s22, - 0x7704a4: LocationName.grass_land_6_s23, - 0x7704a5: LocationName.grass_land_6_s24, - 0x7704a6: LocationName.grass_land_6_s25, - 0x7704a7: LocationName.grass_land_6_s26, - 0x7704a8: LocationName.grass_land_6_s27, - 0x7704a9: LocationName.grass_land_6_s28, - 0x7704aa: LocationName.grass_land_6_s29, - 0x7704ab: LocationName.ripple_field_1_s1, - 0x7704ac: LocationName.ripple_field_1_s2, - 0x7704ad: LocationName.ripple_field_1_s3, - 0x7704ae: LocationName.ripple_field_1_s4, - 0x7704af: LocationName.ripple_field_1_s5, - 0x7704b0: LocationName.ripple_field_1_s6, - 0x7704b1: LocationName.ripple_field_1_s7, - 0x7704b2: LocationName.ripple_field_1_s8, - 0x7704b3: LocationName.ripple_field_1_s9, - 0x7704b4: LocationName.ripple_field_1_s10, - 0x7704b5: LocationName.ripple_field_1_s11, - 0x7704b6: LocationName.ripple_field_1_s12, - 0x7704b7: LocationName.ripple_field_1_s13, - 0x7704b8: LocationName.ripple_field_1_s14, - 0x7704b9: LocationName.ripple_field_1_s15, - 0x7704ba: LocationName.ripple_field_1_s16, - 0x7704bb: LocationName.ripple_field_1_s17, - 0x7704bc: LocationName.ripple_field_1_s18, - 0x7704bd: LocationName.ripple_field_1_s19, - 0x7704be: LocationName.ripple_field_2_s1, - 0x7704bf: LocationName.ripple_field_2_s2, - 0x7704c0: LocationName.ripple_field_2_s3, - 0x7704c1: LocationName.ripple_field_2_s4, - 0x7704c2: LocationName.ripple_field_2_s5, - 0x7704c3: LocationName.ripple_field_2_s6, - 0x7704c4: LocationName.ripple_field_2_s7, - 0x7704c5: LocationName.ripple_field_2_s8, - 0x7704c6: LocationName.ripple_field_2_s9, - 0x7704c7: LocationName.ripple_field_2_s10, - 0x7704c8: LocationName.ripple_field_2_s11, - 0x7704c9: LocationName.ripple_field_2_s12, - 0x7704ca: LocationName.ripple_field_2_s13, - 0x7704cb: LocationName.ripple_field_2_s14, - 0x7704cc: LocationName.ripple_field_2_s15, - 0x7704cd: LocationName.ripple_field_2_s16, - 0x7704ce: LocationName.ripple_field_2_s17, - 0x7704cf: LocationName.ripple_field_3_s1, - 0x7704d0: LocationName.ripple_field_3_s2, - 0x7704d1: LocationName.ripple_field_3_s3, - 0x7704d2: LocationName.ripple_field_3_s4, - 0x7704d3: LocationName.ripple_field_3_s5, - 0x7704d4: LocationName.ripple_field_3_s6, - 0x7704d5: LocationName.ripple_field_3_s7, - 0x7704d6: LocationName.ripple_field_3_s8, - 0x7704d7: LocationName.ripple_field_3_s9, - 0x7704d8: LocationName.ripple_field_3_s10, - 0x7704d9: LocationName.ripple_field_3_s11, - 0x7704da: LocationName.ripple_field_3_s12, - 0x7704db: LocationName.ripple_field_3_s13, - 0x7704dc: LocationName.ripple_field_3_s14, - 0x7704dd: LocationName.ripple_field_3_s15, - 0x7704de: LocationName.ripple_field_3_s16, - 0x7704df: LocationName.ripple_field_3_s17, - 0x7704e0: LocationName.ripple_field_3_s18, - 0x7704e1: LocationName.ripple_field_3_s19, - 0x7704e2: LocationName.ripple_field_3_s20, - 0x7704e3: LocationName.ripple_field_3_s21, - 0x7704e4: LocationName.ripple_field_4_s1, - 0x7704e5: LocationName.ripple_field_4_s2, - 0x7704e6: LocationName.ripple_field_4_s3, - 0x7704e7: LocationName.ripple_field_4_s4, - 0x7704e8: LocationName.ripple_field_4_s5, - 0x7704e9: LocationName.ripple_field_4_s6, - 0x7704ea: LocationName.ripple_field_4_s7, - 0x7704eb: LocationName.ripple_field_4_s8, - 0x7704ec: LocationName.ripple_field_4_s9, - 0x7704ed: LocationName.ripple_field_4_s10, - 0x7704ee: LocationName.ripple_field_4_s11, - 0x7704ef: LocationName.ripple_field_4_s12, - 0x7704f0: LocationName.ripple_field_4_s13, - 0x7704f1: LocationName.ripple_field_4_s14, - 0x7704f2: LocationName.ripple_field_4_s15, - 0x7704f3: LocationName.ripple_field_4_s16, - 0x7704f4: LocationName.ripple_field_4_s17, - 0x7704f5: LocationName.ripple_field_4_s18, - 0x7704f6: LocationName.ripple_field_4_s19, - 0x7704f7: LocationName.ripple_field_4_s20, - 0x7704f8: LocationName.ripple_field_4_s21, - 0x7704f9: LocationName.ripple_field_4_s22, - 0x7704fa: LocationName.ripple_field_4_s23, - 0x7704fb: LocationName.ripple_field_4_s24, - 0x7704fc: LocationName.ripple_field_4_s25, - 0x7704fd: LocationName.ripple_field_4_s26, - 0x7704fe: LocationName.ripple_field_4_s27, - 0x7704ff: LocationName.ripple_field_4_s28, - 0x770500: LocationName.ripple_field_4_s29, - 0x770501: LocationName.ripple_field_4_s30, - 0x770502: LocationName.ripple_field_4_s31, - 0x770503: LocationName.ripple_field_4_s32, - 0x770504: LocationName.ripple_field_4_s33, - 0x770505: LocationName.ripple_field_4_s34, - 0x770506: LocationName.ripple_field_4_s35, - 0x770507: LocationName.ripple_field_4_s36, - 0x770508: LocationName.ripple_field_4_s37, - 0x770509: LocationName.ripple_field_4_s38, - 0x77050a: LocationName.ripple_field_4_s39, - 0x77050b: LocationName.ripple_field_4_s40, - 0x77050c: LocationName.ripple_field_4_s41, - 0x77050d: LocationName.ripple_field_4_s42, - 0x77050e: LocationName.ripple_field_4_s43, - 0x77050f: LocationName.ripple_field_4_s44, - 0x770510: LocationName.ripple_field_4_s45, - 0x770511: LocationName.ripple_field_4_s46, - 0x770512: LocationName.ripple_field_4_s47, - 0x770513: LocationName.ripple_field_4_s48, - 0x770514: LocationName.ripple_field_4_s49, - 0x770515: LocationName.ripple_field_4_s50, - 0x770516: LocationName.ripple_field_4_s51, - 0x770517: LocationName.ripple_field_5_s1, - 0x770518: LocationName.ripple_field_5_s2, - 0x770519: LocationName.ripple_field_5_s3, - 0x77051a: LocationName.ripple_field_5_s4, - 0x77051b: LocationName.ripple_field_5_s5, - 0x77051c: LocationName.ripple_field_5_s6, - 0x77051d: LocationName.ripple_field_5_s7, - 0x77051e: LocationName.ripple_field_5_s8, - 0x77051f: LocationName.ripple_field_5_s9, - 0x770520: LocationName.ripple_field_5_s10, - 0x770521: LocationName.ripple_field_5_s11, - 0x770522: LocationName.ripple_field_5_s12, - 0x770523: LocationName.ripple_field_5_s13, - 0x770524: LocationName.ripple_field_5_s14, - 0x770525: LocationName.ripple_field_5_s15, - 0x770526: LocationName.ripple_field_5_s16, - 0x770527: LocationName.ripple_field_5_s17, - 0x770528: LocationName.ripple_field_5_s18, - 0x770529: LocationName.ripple_field_5_s19, - 0x77052a: LocationName.ripple_field_5_s20, - 0x77052b: LocationName.ripple_field_5_s21, - 0x77052c: LocationName.ripple_field_5_s22, - 0x77052d: LocationName.ripple_field_5_s23, - 0x77052e: LocationName.ripple_field_5_s24, - 0x77052f: LocationName.ripple_field_5_s25, - 0x770530: LocationName.ripple_field_5_s26, - 0x770531: LocationName.ripple_field_5_s27, - 0x770532: LocationName.ripple_field_5_s28, - 0x770533: LocationName.ripple_field_5_s29, - 0x770534: LocationName.ripple_field_5_s30, - 0x770535: LocationName.ripple_field_5_s31, - 0x770536: LocationName.ripple_field_5_s32, - 0x770537: LocationName.ripple_field_5_s33, - 0x770538: LocationName.ripple_field_5_s34, - 0x770539: LocationName.ripple_field_5_s35, - 0x77053a: LocationName.ripple_field_5_s36, - 0x77053b: LocationName.ripple_field_5_s37, - 0x77053c: LocationName.ripple_field_5_s38, - 0x77053d: LocationName.ripple_field_5_s39, - 0x77053e: LocationName.ripple_field_5_s40, - 0x77053f: LocationName.ripple_field_5_s41, - 0x770540: LocationName.ripple_field_5_s42, - 0x770541: LocationName.ripple_field_5_s43, - 0x770542: LocationName.ripple_field_5_s44, - 0x770543: LocationName.ripple_field_5_s45, - 0x770544: LocationName.ripple_field_5_s46, - 0x770545: LocationName.ripple_field_5_s47, - 0x770546: LocationName.ripple_field_5_s48, - 0x770547: LocationName.ripple_field_5_s49, - 0x770548: LocationName.ripple_field_5_s50, - 0x770549: LocationName.ripple_field_5_s51, - 0x77054a: LocationName.ripple_field_6_s1, - 0x77054b: LocationName.ripple_field_6_s2, - 0x77054c: LocationName.ripple_field_6_s3, - 0x77054d: LocationName.ripple_field_6_s4, - 0x77054e: LocationName.ripple_field_6_s5, - 0x77054f: LocationName.ripple_field_6_s6, - 0x770550: LocationName.ripple_field_6_s7, - 0x770551: LocationName.ripple_field_6_s8, - 0x770552: LocationName.ripple_field_6_s9, - 0x770553: LocationName.ripple_field_6_s10, - 0x770554: LocationName.ripple_field_6_s11, - 0x770555: LocationName.ripple_field_6_s12, - 0x770556: LocationName.ripple_field_6_s13, - 0x770557: LocationName.ripple_field_6_s14, - 0x770558: LocationName.ripple_field_6_s15, - 0x770559: LocationName.ripple_field_6_s16, - 0x77055a: LocationName.ripple_field_6_s17, - 0x77055b: LocationName.ripple_field_6_s18, - 0x77055c: LocationName.ripple_field_6_s19, - 0x77055d: LocationName.ripple_field_6_s20, - 0x77055e: LocationName.ripple_field_6_s21, - 0x77055f: LocationName.ripple_field_6_s22, - 0x770560: LocationName.ripple_field_6_s23, - 0x770561: LocationName.sand_canyon_1_s1, - 0x770562: LocationName.sand_canyon_1_s2, - 0x770563: LocationName.sand_canyon_1_s3, - 0x770564: LocationName.sand_canyon_1_s4, - 0x770565: LocationName.sand_canyon_1_s5, - 0x770566: LocationName.sand_canyon_1_s6, - 0x770567: LocationName.sand_canyon_1_s7, - 0x770568: LocationName.sand_canyon_1_s8, - 0x770569: LocationName.sand_canyon_1_s9, - 0x77056a: LocationName.sand_canyon_1_s10, - 0x77056b: LocationName.sand_canyon_1_s11, - 0x77056c: LocationName.sand_canyon_1_s12, - 0x77056d: LocationName.sand_canyon_1_s13, - 0x77056e: LocationName.sand_canyon_1_s14, - 0x77056f: LocationName.sand_canyon_1_s15, - 0x770570: LocationName.sand_canyon_1_s16, - 0x770571: LocationName.sand_canyon_1_s17, - 0x770572: LocationName.sand_canyon_1_s18, - 0x770573: LocationName.sand_canyon_1_s19, - 0x770574: LocationName.sand_canyon_1_s20, - 0x770575: LocationName.sand_canyon_1_s21, - 0x770576: LocationName.sand_canyon_1_s22, - 0x770577: LocationName.sand_canyon_2_s1, - 0x770578: LocationName.sand_canyon_2_s2, - 0x770579: LocationName.sand_canyon_2_s3, - 0x77057a: LocationName.sand_canyon_2_s4, - 0x77057b: LocationName.sand_canyon_2_s5, - 0x77057c: LocationName.sand_canyon_2_s6, - 0x77057d: LocationName.sand_canyon_2_s7, - 0x77057e: LocationName.sand_canyon_2_s8, - 0x77057f: LocationName.sand_canyon_2_s9, - 0x770580: LocationName.sand_canyon_2_s10, - 0x770581: LocationName.sand_canyon_2_s11, - 0x770582: LocationName.sand_canyon_2_s12, - 0x770583: LocationName.sand_canyon_2_s13, - 0x770584: LocationName.sand_canyon_2_s14, - 0x770585: LocationName.sand_canyon_2_s15, - 0x770586: LocationName.sand_canyon_2_s16, - 0x770587: LocationName.sand_canyon_2_s17, - 0x770588: LocationName.sand_canyon_2_s18, - 0x770589: LocationName.sand_canyon_2_s19, - 0x77058a: LocationName.sand_canyon_2_s20, - 0x77058b: LocationName.sand_canyon_2_s21, - 0x77058c: LocationName.sand_canyon_2_s22, - 0x77058d: LocationName.sand_canyon_2_s23, - 0x77058e: LocationName.sand_canyon_2_s24, - 0x77058f: LocationName.sand_canyon_2_s25, - 0x770590: LocationName.sand_canyon_2_s26, - 0x770591: LocationName.sand_canyon_2_s27, - 0x770592: LocationName.sand_canyon_2_s28, - 0x770593: LocationName.sand_canyon_2_s29, - 0x770594: LocationName.sand_canyon_2_s30, - 0x770595: LocationName.sand_canyon_2_s31, - 0x770596: LocationName.sand_canyon_2_s32, - 0x770597: LocationName.sand_canyon_2_s33, - 0x770598: LocationName.sand_canyon_2_s34, - 0x770599: LocationName.sand_canyon_2_s35, - 0x77059a: LocationName.sand_canyon_2_s36, - 0x77059b: LocationName.sand_canyon_2_s37, - 0x77059c: LocationName.sand_canyon_2_s38, - 0x77059d: LocationName.sand_canyon_2_s39, - 0x77059e: LocationName.sand_canyon_2_s40, - 0x77059f: LocationName.sand_canyon_2_s41, - 0x7705a0: LocationName.sand_canyon_2_s42, - 0x7705a1: LocationName.sand_canyon_2_s43, - 0x7705a2: LocationName.sand_canyon_2_s44, - 0x7705a3: LocationName.sand_canyon_2_s45, - 0x7705a4: LocationName.sand_canyon_2_s46, - 0x7705a5: LocationName.sand_canyon_2_s47, - 0x7705a6: LocationName.sand_canyon_2_s48, - 0x7705a7: LocationName.sand_canyon_3_s1, - 0x7705a8: LocationName.sand_canyon_3_s2, - 0x7705a9: LocationName.sand_canyon_3_s3, - 0x7705aa: LocationName.sand_canyon_3_s4, - 0x7705ab: LocationName.sand_canyon_3_s5, - 0x7705ac: LocationName.sand_canyon_3_s6, - 0x7705ad: LocationName.sand_canyon_3_s7, - 0x7705ae: LocationName.sand_canyon_3_s8, - 0x7705af: LocationName.sand_canyon_3_s9, - 0x7705b0: LocationName.sand_canyon_3_s10, - 0x7705b1: LocationName.sand_canyon_4_s1, - 0x7705b2: LocationName.sand_canyon_4_s2, - 0x7705b3: LocationName.sand_canyon_4_s3, - 0x7705b4: LocationName.sand_canyon_4_s4, - 0x7705b5: LocationName.sand_canyon_4_s5, - 0x7705b6: LocationName.sand_canyon_4_s6, - 0x7705b7: LocationName.sand_canyon_4_s7, - 0x7705b8: LocationName.sand_canyon_4_s8, - 0x7705b9: LocationName.sand_canyon_4_s9, - 0x7705ba: LocationName.sand_canyon_4_s10, - 0x7705bb: LocationName.sand_canyon_4_s11, - 0x7705bc: LocationName.sand_canyon_4_s12, - 0x7705bd: LocationName.sand_canyon_4_s13, - 0x7705be: LocationName.sand_canyon_4_s14, - 0x7705bf: LocationName.sand_canyon_4_s15, - 0x7705c0: LocationName.sand_canyon_4_s16, - 0x7705c1: LocationName.sand_canyon_4_s17, - 0x7705c2: LocationName.sand_canyon_4_s18, - 0x7705c3: LocationName.sand_canyon_4_s19, - 0x7705c4: LocationName.sand_canyon_4_s20, - 0x7705c5: LocationName.sand_canyon_4_s21, - 0x7705c6: LocationName.sand_canyon_4_s22, - 0x7705c7: LocationName.sand_canyon_4_s23, - 0x7705c8: LocationName.sand_canyon_5_s1, - 0x7705c9: LocationName.sand_canyon_5_s2, - 0x7705ca: LocationName.sand_canyon_5_s3, - 0x7705cb: LocationName.sand_canyon_5_s4, - 0x7705cc: LocationName.sand_canyon_5_s5, - 0x7705cd: LocationName.sand_canyon_5_s6, - 0x7705ce: LocationName.sand_canyon_5_s7, - 0x7705cf: LocationName.sand_canyon_5_s8, - 0x7705d0: LocationName.sand_canyon_5_s9, - 0x7705d1: LocationName.sand_canyon_5_s10, - 0x7705d2: LocationName.sand_canyon_5_s11, - 0x7705d3: LocationName.sand_canyon_5_s12, - 0x7705d4: LocationName.sand_canyon_5_s13, - 0x7705d5: LocationName.sand_canyon_5_s14, - 0x7705d6: LocationName.sand_canyon_5_s15, - 0x7705d7: LocationName.sand_canyon_5_s16, - 0x7705d8: LocationName.sand_canyon_5_s17, - 0x7705d9: LocationName.sand_canyon_5_s18, - 0x7705da: LocationName.sand_canyon_5_s19, - 0x7705db: LocationName.sand_canyon_5_s20, - 0x7705dc: LocationName.sand_canyon_5_s21, - 0x7705dd: LocationName.sand_canyon_5_s22, - 0x7705de: LocationName.sand_canyon_5_s23, - 0x7705df: LocationName.sand_canyon_5_s24, - 0x7705e0: LocationName.sand_canyon_5_s25, - 0x7705e1: LocationName.sand_canyon_5_s26, - 0x7705e2: LocationName.sand_canyon_5_s27, - 0x7705e3: LocationName.sand_canyon_5_s28, - 0x7705e4: LocationName.sand_canyon_5_s29, - 0x7705e5: LocationName.sand_canyon_5_s30, - 0x7705e6: LocationName.sand_canyon_5_s31, - 0x7705e7: LocationName.sand_canyon_5_s32, - 0x7705e8: LocationName.sand_canyon_5_s33, - 0x7705e9: LocationName.sand_canyon_5_s34, - 0x7705ea: LocationName.sand_canyon_5_s35, - 0x7705eb: LocationName.sand_canyon_5_s36, - 0x7705ec: LocationName.sand_canyon_5_s37, - 0x7705ed: LocationName.sand_canyon_5_s38, - 0x7705ee: LocationName.sand_canyon_5_s39, - 0x7705ef: LocationName.sand_canyon_5_s40, - 0x7705f0: LocationName.cloudy_park_1_s1, - 0x7705f1: LocationName.cloudy_park_1_s2, - 0x7705f2: LocationName.cloudy_park_1_s3, - 0x7705f3: LocationName.cloudy_park_1_s4, - 0x7705f4: LocationName.cloudy_park_1_s5, - 0x7705f5: LocationName.cloudy_park_1_s6, - 0x7705f6: LocationName.cloudy_park_1_s7, - 0x7705f7: LocationName.cloudy_park_1_s8, - 0x7705f8: LocationName.cloudy_park_1_s9, - 0x7705f9: LocationName.cloudy_park_1_s10, - 0x7705fa: LocationName.cloudy_park_1_s11, - 0x7705fb: LocationName.cloudy_park_1_s12, - 0x7705fc: LocationName.cloudy_park_1_s13, - 0x7705fd: LocationName.cloudy_park_1_s14, - 0x7705fe: LocationName.cloudy_park_1_s15, - 0x7705ff: LocationName.cloudy_park_1_s16, - 0x770600: LocationName.cloudy_park_1_s17, - 0x770601: LocationName.cloudy_park_1_s18, - 0x770602: LocationName.cloudy_park_1_s19, - 0x770603: LocationName.cloudy_park_1_s20, - 0x770604: LocationName.cloudy_park_1_s21, - 0x770605: LocationName.cloudy_park_1_s22, - 0x770606: LocationName.cloudy_park_1_s23, - 0x770607: LocationName.cloudy_park_2_s1, - 0x770608: LocationName.cloudy_park_2_s2, - 0x770609: LocationName.cloudy_park_2_s3, - 0x77060a: LocationName.cloudy_park_2_s4, - 0x77060b: LocationName.cloudy_park_2_s5, - 0x77060c: LocationName.cloudy_park_2_s6, - 0x77060d: LocationName.cloudy_park_2_s7, - 0x77060e: LocationName.cloudy_park_2_s8, - 0x77060f: LocationName.cloudy_park_2_s9, - 0x770610: LocationName.cloudy_park_2_s10, - 0x770611: LocationName.cloudy_park_2_s11, - 0x770612: LocationName.cloudy_park_2_s12, - 0x770613: LocationName.cloudy_park_2_s13, - 0x770614: LocationName.cloudy_park_2_s14, - 0x770615: LocationName.cloudy_park_2_s15, - 0x770616: LocationName.cloudy_park_2_s16, - 0x770617: LocationName.cloudy_park_2_s17, - 0x770618: LocationName.cloudy_park_2_s18, - 0x770619: LocationName.cloudy_park_2_s19, - 0x77061a: LocationName.cloudy_park_2_s20, - 0x77061b: LocationName.cloudy_park_2_s21, - 0x77061c: LocationName.cloudy_park_2_s22, - 0x77061d: LocationName.cloudy_park_2_s23, - 0x77061e: LocationName.cloudy_park_2_s24, - 0x77061f: LocationName.cloudy_park_2_s25, - 0x770620: LocationName.cloudy_park_2_s26, - 0x770621: LocationName.cloudy_park_2_s27, - 0x770622: LocationName.cloudy_park_2_s28, - 0x770623: LocationName.cloudy_park_2_s29, - 0x770624: LocationName.cloudy_park_2_s30, - 0x770625: LocationName.cloudy_park_2_s31, - 0x770626: LocationName.cloudy_park_2_s32, - 0x770627: LocationName.cloudy_park_2_s33, - 0x770628: LocationName.cloudy_park_2_s34, - 0x770629: LocationName.cloudy_park_2_s35, - 0x77062a: LocationName.cloudy_park_2_s36, - 0x77062b: LocationName.cloudy_park_2_s37, - 0x77062c: LocationName.cloudy_park_2_s38, - 0x77062d: LocationName.cloudy_park_2_s39, - 0x77062e: LocationName.cloudy_park_2_s40, - 0x77062f: LocationName.cloudy_park_2_s41, - 0x770630: LocationName.cloudy_park_2_s42, - 0x770631: LocationName.cloudy_park_2_s43, - 0x770632: LocationName.cloudy_park_2_s44, - 0x770633: LocationName.cloudy_park_2_s45, - 0x770634: LocationName.cloudy_park_2_s46, - 0x770635: LocationName.cloudy_park_2_s47, - 0x770636: LocationName.cloudy_park_2_s48, - 0x770637: LocationName.cloudy_park_2_s49, - 0x770638: LocationName.cloudy_park_2_s50, - 0x770639: LocationName.cloudy_park_2_s51, - 0x77063a: LocationName.cloudy_park_2_s52, - 0x77063b: LocationName.cloudy_park_2_s53, - 0x77063c: LocationName.cloudy_park_2_s54, - 0x77063d: LocationName.cloudy_park_3_s1, - 0x77063e: LocationName.cloudy_park_3_s2, - 0x77063f: LocationName.cloudy_park_3_s3, - 0x770640: LocationName.cloudy_park_3_s4, - 0x770641: LocationName.cloudy_park_3_s5, - 0x770642: LocationName.cloudy_park_3_s6, - 0x770643: LocationName.cloudy_park_3_s7, - 0x770644: LocationName.cloudy_park_3_s8, - 0x770645: LocationName.cloudy_park_3_s9, - 0x770646: LocationName.cloudy_park_3_s10, - 0x770647: LocationName.cloudy_park_3_s11, - 0x770648: LocationName.cloudy_park_3_s12, - 0x770649: LocationName.cloudy_park_3_s13, - 0x77064a: LocationName.cloudy_park_3_s14, - 0x77064b: LocationName.cloudy_park_3_s15, - 0x77064c: LocationName.cloudy_park_3_s16, - 0x77064d: LocationName.cloudy_park_3_s17, - 0x77064e: LocationName.cloudy_park_3_s18, - 0x77064f: LocationName.cloudy_park_3_s19, - 0x770650: LocationName.cloudy_park_3_s20, - 0x770651: LocationName.cloudy_park_3_s21, - 0x770652: LocationName.cloudy_park_3_s22, - 0x770653: LocationName.cloudy_park_4_s1, - 0x770654: LocationName.cloudy_park_4_s2, - 0x770655: LocationName.cloudy_park_4_s3, - 0x770656: LocationName.cloudy_park_4_s4, - 0x770657: LocationName.cloudy_park_4_s5, - 0x770658: LocationName.cloudy_park_4_s6, - 0x770659: LocationName.cloudy_park_4_s7, - 0x77065a: LocationName.cloudy_park_4_s8, - 0x77065b: LocationName.cloudy_park_4_s9, - 0x77065c: LocationName.cloudy_park_4_s10, - 0x77065d: LocationName.cloudy_park_4_s11, - 0x77065e: LocationName.cloudy_park_4_s12, - 0x77065f: LocationName.cloudy_park_4_s13, - 0x770660: LocationName.cloudy_park_4_s14, - 0x770661: LocationName.cloudy_park_4_s15, - 0x770662: LocationName.cloudy_park_4_s16, - 0x770663: LocationName.cloudy_park_4_s17, - 0x770664: LocationName.cloudy_park_4_s18, - 0x770665: LocationName.cloudy_park_4_s19, - 0x770666: LocationName.cloudy_park_4_s20, - 0x770667: LocationName.cloudy_park_4_s21, - 0x770668: LocationName.cloudy_park_4_s22, - 0x770669: LocationName.cloudy_park_4_s23, - 0x77066a: LocationName.cloudy_park_4_s24, - 0x77066b: LocationName.cloudy_park_4_s25, - 0x77066c: LocationName.cloudy_park_4_s26, - 0x77066d: LocationName.cloudy_park_4_s27, - 0x77066e: LocationName.cloudy_park_4_s28, - 0x77066f: LocationName.cloudy_park_4_s29, - 0x770670: LocationName.cloudy_park_4_s30, - 0x770671: LocationName.cloudy_park_4_s31, - 0x770672: LocationName.cloudy_park_4_s32, - 0x770673: LocationName.cloudy_park_4_s33, - 0x770674: LocationName.cloudy_park_4_s34, - 0x770675: LocationName.cloudy_park_4_s35, - 0x770676: LocationName.cloudy_park_4_s36, - 0x770677: LocationName.cloudy_park_4_s37, - 0x770678: LocationName.cloudy_park_4_s38, - 0x770679: LocationName.cloudy_park_4_s39, - 0x77067a: LocationName.cloudy_park_4_s40, - 0x77067b: LocationName.cloudy_park_4_s41, - 0x77067c: LocationName.cloudy_park_4_s42, - 0x77067d: LocationName.cloudy_park_4_s43, - 0x77067e: LocationName.cloudy_park_4_s44, - 0x77067f: LocationName.cloudy_park_4_s45, - 0x770680: LocationName.cloudy_park_4_s46, - 0x770681: LocationName.cloudy_park_4_s47, - 0x770682: LocationName.cloudy_park_4_s48, - 0x770683: LocationName.cloudy_park_4_s49, - 0x770684: LocationName.cloudy_park_4_s50, - 0x770685: LocationName.cloudy_park_5_s1, - 0x770686: LocationName.cloudy_park_5_s2, - 0x770687: LocationName.cloudy_park_5_s3, - 0x770688: LocationName.cloudy_park_5_s4, - 0x770689: LocationName.cloudy_park_5_s5, - 0x77068a: LocationName.cloudy_park_5_s6, - 0x77068b: LocationName.cloudy_park_6_s1, - 0x77068c: LocationName.cloudy_park_6_s2, - 0x77068d: LocationName.cloudy_park_6_s3, - 0x77068e: LocationName.cloudy_park_6_s4, - 0x77068f: LocationName.cloudy_park_6_s5, - 0x770690: LocationName.cloudy_park_6_s6, - 0x770691: LocationName.cloudy_park_6_s7, - 0x770692: LocationName.cloudy_park_6_s8, - 0x770693: LocationName.cloudy_park_6_s9, - 0x770694: LocationName.cloudy_park_6_s10, - 0x770695: LocationName.cloudy_park_6_s11, - 0x770696: LocationName.cloudy_park_6_s12, - 0x770697: LocationName.cloudy_park_6_s13, - 0x770698: LocationName.cloudy_park_6_s14, - 0x770699: LocationName.cloudy_park_6_s15, - 0x77069a: LocationName.cloudy_park_6_s16, - 0x77069b: LocationName.cloudy_park_6_s17, - 0x77069c: LocationName.cloudy_park_6_s18, - 0x77069d: LocationName.cloudy_park_6_s19, - 0x77069e: LocationName.cloudy_park_6_s20, - 0x77069f: LocationName.cloudy_park_6_s21, - 0x7706a0: LocationName.cloudy_park_6_s22, - 0x7706a1: LocationName.cloudy_park_6_s23, - 0x7706a2: LocationName.cloudy_park_6_s24, - 0x7706a3: LocationName.cloudy_park_6_s25, - 0x7706a4: LocationName.cloudy_park_6_s26, - 0x7706a5: LocationName.cloudy_park_6_s27, - 0x7706a6: LocationName.cloudy_park_6_s28, - 0x7706a7: LocationName.cloudy_park_6_s29, - 0x7706a8: LocationName.cloudy_park_6_s30, - 0x7706a9: LocationName.cloudy_park_6_s31, - 0x7706aa: LocationName.cloudy_park_6_s32, - 0x7706ab: LocationName.cloudy_park_6_s33, - 0x7706ac: LocationName.iceberg_1_s1, - 0x7706ad: LocationName.iceberg_1_s2, - 0x7706ae: LocationName.iceberg_1_s3, - 0x7706af: LocationName.iceberg_1_s4, - 0x7706b0: LocationName.iceberg_1_s5, - 0x7706b1: LocationName.iceberg_1_s6, - 0x7706b2: LocationName.iceberg_2_s1, - 0x7706b3: LocationName.iceberg_2_s2, - 0x7706b4: LocationName.iceberg_2_s3, - 0x7706b5: LocationName.iceberg_2_s4, - 0x7706b6: LocationName.iceberg_2_s5, - 0x7706b7: LocationName.iceberg_2_s6, - 0x7706b8: LocationName.iceberg_2_s7, - 0x7706b9: LocationName.iceberg_2_s8, - 0x7706ba: LocationName.iceberg_2_s9, - 0x7706bb: LocationName.iceberg_2_s10, - 0x7706bc: LocationName.iceberg_2_s11, - 0x7706bd: LocationName.iceberg_2_s12, - 0x7706be: LocationName.iceberg_2_s13, - 0x7706bf: LocationName.iceberg_2_s14, - 0x7706c0: LocationName.iceberg_2_s15, - 0x7706c1: LocationName.iceberg_2_s16, - 0x7706c2: LocationName.iceberg_2_s17, - 0x7706c3: LocationName.iceberg_2_s18, - 0x7706c4: LocationName.iceberg_2_s19, - 0x7706c5: LocationName.iceberg_3_s1, - 0x7706c6: LocationName.iceberg_3_s2, - 0x7706c7: LocationName.iceberg_3_s3, - 0x7706c8: LocationName.iceberg_3_s4, - 0x7706c9: LocationName.iceberg_3_s5, - 0x7706ca: LocationName.iceberg_3_s6, - 0x7706cb: LocationName.iceberg_3_s7, - 0x7706cc: LocationName.iceberg_3_s8, - 0x7706cd: LocationName.iceberg_3_s9, - 0x7706ce: LocationName.iceberg_3_s10, - 0x7706cf: LocationName.iceberg_3_s11, - 0x7706d0: LocationName.iceberg_3_s12, - 0x7706d1: LocationName.iceberg_3_s13, - 0x7706d2: LocationName.iceberg_3_s14, - 0x7706d3: LocationName.iceberg_3_s15, - 0x7706d4: LocationName.iceberg_3_s16, - 0x7706d5: LocationName.iceberg_3_s17, - 0x7706d6: LocationName.iceberg_3_s18, - 0x7706d7: LocationName.iceberg_3_s19, - 0x7706d8: LocationName.iceberg_3_s20, - 0x7706d9: LocationName.iceberg_3_s21, - 0x7706da: LocationName.iceberg_4_s1, - 0x7706db: LocationName.iceberg_4_s2, - 0x7706dc: LocationName.iceberg_4_s3, - 0x7706dd: LocationName.iceberg_5_s1, - 0x7706de: LocationName.iceberg_5_s2, - 0x7706df: LocationName.iceberg_5_s3, - 0x7706e0: LocationName.iceberg_5_s4, - 0x7706e1: LocationName.iceberg_5_s5, - 0x7706e2: LocationName.iceberg_5_s6, - 0x7706e3: LocationName.iceberg_5_s7, - 0x7706e4: LocationName.iceberg_5_s8, - 0x7706e5: LocationName.iceberg_5_s9, - 0x7706e6: LocationName.iceberg_5_s10, - 0x7706e7: LocationName.iceberg_5_s11, - 0x7706e8: LocationName.iceberg_5_s12, - 0x7706e9: LocationName.iceberg_5_s13, - 0x7706ea: LocationName.iceberg_5_s14, - 0x7706eb: LocationName.iceberg_5_s15, - 0x7706ec: LocationName.iceberg_5_s16, - 0x7706ed: LocationName.iceberg_5_s17, - 0x7706ee: LocationName.iceberg_5_s18, - 0x7706ef: LocationName.iceberg_5_s19, - 0x7706f0: LocationName.iceberg_5_s20, - 0x7706f1: LocationName.iceberg_5_s21, - 0x7706f2: LocationName.iceberg_5_s22, - 0x7706f3: LocationName.iceberg_5_s23, - 0x7706f4: LocationName.iceberg_5_s24, - 0x7706f5: LocationName.iceberg_5_s25, - 0x7706f6: LocationName.iceberg_5_s26, - 0x7706f7: LocationName.iceberg_5_s27, - 0x7706f8: LocationName.iceberg_5_s28, - 0x7706f9: LocationName.iceberg_5_s29, - 0x7706fa: LocationName.iceberg_5_s30, - 0x7706fb: LocationName.iceberg_5_s31, - 0x7706fc: LocationName.iceberg_5_s32, - 0x7706fd: LocationName.iceberg_5_s33, - 0x7706fe: LocationName.iceberg_5_s34, - 0x7706ff: LocationName.iceberg_6_s1, - -} - -location_table = { - **stage_locations, - **heart_star_locations, - **boss_locations, - **consumable_locations, - **star_locations -} diff --git a/worlds/kdl3/Rom.py b/worlds/kdl3/Rom.py deleted file mode 100644 index 5a846ab8be5e..000000000000 --- a/worlds/kdl3/Rom.py +++ /dev/null @@ -1,577 +0,0 @@ -import typing -from pkgutil import get_data - -import Utils -from typing import Optional, TYPE_CHECKING -import hashlib -import os -import struct - -import settings -from worlds.Files import APDeltaPatch -from .Aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \ - get_gooey_palette -from .Compression import hal_decompress -import bsdiff4 - -if TYPE_CHECKING: - from . import KDL3World - -KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2" -KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2" - -level_pointers = { - 0x770001: 0x0084, - 0x770002: 0x009C, - 0x770003: 0x00B8, - 0x770004: 0x00D8, - 0x770005: 0x0104, - 0x770006: 0x0124, - 0x770007: 0x014C, - 0x770008: 0x0170, - 0x770009: 0x0190, - 0x77000A: 0x01B0, - 0x77000B: 0x01E8, - 0x77000C: 0x0218, - 0x77000D: 0x024C, - 0x77000E: 0x0270, - 0x77000F: 0x02A0, - 0x770010: 0x02C4, - 0x770011: 0x02EC, - 0x770012: 0x0314, - 0x770013: 0x03CC, - 0x770014: 0x0404, - 0x770015: 0x042C, - 0x770016: 0x044C, - 0x770017: 0x0478, - 0x770018: 0x049C, - 0x770019: 0x04E4, - 0x77001A: 0x0504, - 0x77001B: 0x0530, - 0x77001C: 0x0554, - 0x77001D: 0x05A8, - 0x77001E: 0x0640, - 0x770200: 0x0148, - 0x770201: 0x0248, - 0x770202: 0x03C8, - 0x770203: 0x04E0, - 0x770204: 0x06A4, - 0x770205: 0x06A8, -} - -bb_bosses = { - 0x770200: 0xED85F1, - 0x770201: 0xF01360, - 0x770202: 0xEDA3DF, - 0x770203: 0xEDC2B9, - 0x770204: 0xED7C3F, - 0x770205: 0xEC29D2, -} - -level_sprites = { - 0x19B2C6: 1827, - 0x1A195C: 1584, - 0x19F6F3: 1679, - 0x19DC8B: 1717, - 0x197900: 1872 -} - -stage_tiles = { - 0: [ - 0, 1, 2, - 16, 17, 18, - 32, 33, 34, - 48, 49, 50 - ], - 1: [ - 3, 4, 5, - 19, 20, 21, - 35, 36, 37, - 51, 52, 53 - ], - 2: [ - 6, 7, 8, - 22, 23, 24, - 38, 39, 40, - 54, 55, 56 - ], - 3: [ - 9, 10, 11, - 25, 26, 27, - 41, 42, 43, - 57, 58, 59, - ], - 4: [ - 12, 13, 64, - 28, 29, 65, - 44, 45, 66, - 60, 61, 67 - ], - 5: [ - 14, 15, 68, - 30, 31, 69, - 46, 47, 70, - 62, 63, 71 - ] -} - -heart_star_address = 0x2D0000 -heart_star_size = 456 -consumable_address = 0x2F91DD -consumable_size = 698 - -stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164] - -music_choices = [ - 2, # Boss 1 - 3, # Boss 2 (Unused) - 4, # Boss 3 (Miniboss) - 7, # Dedede - 9, # Event 2 (used once) - 10, # Field 1 - 11, # Field 2 - 12, # Field 3 - 13, # Field 4 - 14, # Field 5 - 15, # Field 6 - 16, # Field 7 - 17, # Field 8 - 18, # Field 9 - 19, # Field 10 - 20, # Field 11 - 21, # Field 12 (Gourmet Race) - 23, # Dark Matter in the Hyper Zone - 24, # Zero - 25, # Level 1 - 26, # Level 2 - 27, # Level 4 - 28, # Level 3 - 29, # Heart Star Failed - 30, # Level 5 - 31, # Minigame - 38, # Animal Friend 1 - 39, # Animal Friend 2 - 40, # Animal Friend 3 -] -# extra room pointers we don't want to track other than for music -room_pointers = [ - 3079990, # Zero - 2983409, # BB Whispy - 3150688, # BB Acro - 2991071, # BB PonCon - 2998969, # BB Ado - 2980927, # BB Dedede - 2894290 # BB Zero -] - -enemy_remap = { - "Waddle Dee": 0, - "Bronto Burt": 2, - "Rocky": 3, - "Bobo": 5, - "Chilly": 6, - "Poppy Bros Jr.": 7, - "Sparky": 8, - "Polof": 9, - "Broom Hatter": 11, - "Cappy": 12, - "Bouncy": 13, - "Nruff": 15, - "Glunk": 16, - "Togezo": 18, - "Kabu": 19, - "Mony": 20, - "Blipper": 21, - "Squishy": 22, - "Gabon": 24, - "Oro": 25, - "Galbo": 26, - "Sir Kibble": 27, - "Nidoo": 28, - "Kany": 29, - "Sasuke": 30, - "Yaban": 32, - "Boten": 33, - "Coconut": 34, - "Doka": 35, - "Icicle": 36, - "Pteran": 39, - "Loud": 40, - "Como": 41, - "Klinko": 42, - "Babut": 43, - "Wappa": 44, - "Mariel": 45, - "Tick": 48, - "Apolo": 49, - "Popon Ball": 50, - "KeKe": 51, - "Magoo": 53, - "Raft Waddle Dee": 57, - "Madoo": 58, - "Corori": 60, - "Kapar": 67, - "Batamon": 68, - "Peran": 72, - "Bobin": 73, - "Mopoo": 74, - "Gansan": 75, - "Bukiset (Burning)": 76, - "Bukiset (Stone)": 77, - "Bukiset (Ice)": 78, - "Bukiset (Needle)": 79, - "Bukiset (Clean)": 80, - "Bukiset (Parasol)": 81, - "Bukiset (Spark)": 82, - "Bukiset (Cutter)": 83, - "Waddle Dee Drawing": 84, - "Bronto Burt Drawing": 85, - "Bouncy Drawing": 86, - "Kabu (Dekabu)": 87, - "Wapod": 88, - "Propeller": 89, - "Dogon": 90, - "Joe": 91 -} - -miniboss_remap = { - "Captain Stitch": 0, - "Yuki": 1, - "Blocky": 2, - "Jumper Shoot": 3, - "Boboo": 4, - "Haboki": 5 -} - -ability_remap = { - "No Ability": 0, - "Burning Ability": 1, - "Stone Ability": 2, - "Ice Ability": 3, - "Needle Ability": 4, - "Clean Ability": 5, - "Parasol Ability": 6, - "Spark Ability": 7, - "Cutter Ability": 8, -} - - -class RomData: - def __init__(self, file: str, name: typing.Optional[str] = None): - self.file = bytearray() - self.read_from_file(file) - self.name = name - - def read_byte(self, offset: int): - return self.file[offset] - - def read_bytes(self, offset: int, length: int): - return self.file[offset:offset + length] - - def write_byte(self, offset: int, value: int): - self.file[offset] = value - - def write_bytes(self, offset: int, values: typing.Sequence) -> None: - self.file[offset:offset + len(values)] = values - - def write_to_file(self, file: str): - with open(file, 'wb') as outfile: - outfile.write(self.file) - - def read_from_file(self, file: str): - with open(file, 'rb') as stream: - self.file = bytearray(stream.read()) - - def apply_patch(self, patch: bytes): - self.file = bytearray(bsdiff4.patch(bytes(self.file), patch)) - - def write_crc(self): - crc = (sum(self.file[:0x7FDC] + self.file[0x7FE0:]) + 0x01FE) & 0xFFFF - inv = crc ^ 0xFFFF - self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]) - - -def handle_level_sprites(stages, sprites, palettes): - palette_by_level = list() - for palette in palettes: - palette_by_level.extend(palette[10:16]) - for i in range(5): - for j in range(6): - palettes[i][10 + j] = palette_by_level[stages[i][j] - 1] - palettes[i] = [x for palette in palettes[i] for x in palette] - tiles_by_level = list() - for spritesheet in sprites: - decompressed = hal_decompress(spritesheet) - tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)] - tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles]) - for world in range(5): - levels = [stages[world][x] - 1 for x in range(6)] - world_tiles: typing.List[typing.Optional[bytes]] = [None for _ in range(72)] - for i in range(6): - for x in range(12): - world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x] - sprites[world] = list() - for tile in world_tiles: - sprites[world].extend(tile) - # insert our fake compression - sprites[world][0:0] = [0xe3, 0xff] - sprites[world][1026:1026] = [0xe3, 0xff] - sprites[world][2052:2052] = [0xe0, 0xff] - sprites[world].append(0xff) - return sprites, palettes - - -def write_heart_star_sprites(rom: RomData): - compressed = rom.read_bytes(heart_star_address, heart_star_size) - decompressed = hal_decompress(compressed) - patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4")) - patched = bytearray(bsdiff4.patch(decompressed, patch)) - rom.write_bytes(0x1AF7DF, patched) - patched[0:0] = [0xE3, 0xFF] - patched.append(0xFF) - rom.write_bytes(0x1CD000, patched) - rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39]) - - -def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool): - compressed = rom.read_bytes(consumable_address, consumable_size) - decompressed = hal_decompress(compressed) - patched = bytearray(decompressed) - if consumables: - patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4")) - patched = bytearray(bsdiff4.patch(bytes(patched), patch)) - if stars: - patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4")) - patched = bytearray(bsdiff4.patch(bytes(patched), patch)) - patched[0:0] = [0xE3, 0xFF] - patched.append(0xFF) - rom.write_bytes(0x1CD500, patched) - rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39]) - - -class KDL3DeltaPatch(APDeltaPatch): - hash = [KDL3UHASH, KDL3JHASH] - game = "Kirby's Dream Land 3" - patch_file_ending = ".apkdl3" - - @classmethod - def get_source_data(cls) -> bytes: - return get_base_rom_bytes() - - def patch(self, target: str): - super().patch(target) - rom = RomData(target) - target_language = rom.read_byte(0x3C020) - rom.write_byte(0x7FD9, target_language) - write_heart_star_sprites(rom) - if rom.read_bytes(0x3D014, 1)[0] > 0: - stages = [struct.unpack("HHHHHHH", rom.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)] - palettes = [rom.read_bytes(full_pal, 512) for full_pal in stage_palettes] - palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes] - sprites = [rom.read_bytes(offset, level_sprites[offset]) for offset in level_sprites] - sprites, palettes = handle_level_sprites(stages, sprites, palettes) - for addr, palette in zip(stage_palettes, palettes): - rom.write_bytes(addr, palette) - for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites): - rom.write_bytes(addr, level_sprite) - rom.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39, - 0x50, 0xC4, 0x39]) - write_consumable_sprites(rom, rom.read_byte(0x3D018) > 0, rom.read_byte(0x3D01A) > 0) - rom_name = rom.read_bytes(0x3C000, 21) - rom.write_bytes(0x7FC0, rom_name) - rom.write_crc() - rom.write_to_file(target) - - -def patch_rom(world: "KDL3World", rom: RomData): - rom.apply_patch(get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4"))) - tiles = get_data(__name__, os.path.join("data", "APPauseIcons.dat")) - rom.write_bytes(0x3F000, tiles) - - # Write open world patch - if world.options.open_world: - rom.write_bytes(0x143C7, [0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ]) - # changes the stage flag function to compare $5AC1 to $5AC1, - # always running the "new stage" function - # This has further checks present for bosses already, so we just - # need to handle regular stages - # write check for boss to be unlocked - - if world.options.consumables: - # reroute maxim tomatoes to use the 1-UP function, then null out the function - rom.write_bytes(0x3002F, [0x37, 0x00]) - rom.write_bytes(0x30037, [0xA9, 0x26, 0x00, # LDA #$0026 - 0x22, 0x27, 0xD9, 0x00, # JSL $00D927 - 0xA4, 0xD2, # LDY $D2 - 0x6B, # RTL - 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, # NOP #10 - ]) - - # stars handling is built into the rom, so no changes there - - rooms = world.rooms - if world.options.music_shuffle > 0: - if world.options.music_shuffle == 1: - shuffled_music = music_choices.copy() - world.random.shuffle(shuffled_music) - music_map = dict(zip(music_choices, shuffled_music)) - # Avoid putting star twinkle in the pool - music_map[5] = world.random.choice(music_choices) - # Heart Star music doesn't work on regular stages - music_map[8] = world.random.choice(music_choices) - for room in rooms: - room.music = music_map[room.music] - for room in room_pointers: - old_music = rom.read_byte(room + 2) - rom.write_byte(room + 2, music_map[old_music]) - for i in range(5): - # level themes - old_music = rom.read_byte(0x133F2 + i) - rom.write_byte(0x133F2 + i, music_map[old_music]) - # Zero - rom.write_byte(0x9AE79, music_map[0x18]) - # Heart Star success and fail - rom.write_byte(0x4A388, music_map[0x08]) - rom.write_byte(0x4A38D, music_map[0x1D]) - elif world.options.music_shuffle == 2: - for room in rooms: - room.music = world.random.choice(music_choices) - for room in room_pointers: - rom.write_byte(room + 2, world.random.choice(music_choices)) - for i in range(5): - # level themes - rom.write_byte(0x133F2 + i, world.random.choice(music_choices)) - # Zero - rom.write_byte(0x9AE79, world.random.choice(music_choices)) - # Heart Star success and fail - rom.write_byte(0x4A388, world.random.choice(music_choices)) - rom.write_byte(0x4A38D, world.random.choice(music_choices)) - - for room in rooms: - room.patch(rom) - - if world.options.virtual_console in [1, 3]: - # Flash Reduction - rom.write_byte(0x9AE68, 0x10) - rom.write_bytes(0x9AE8E, [0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ]) - rom.write_byte(0x9AEA1, 0x08) - rom.write_byte(0x9AEC9, 0x01) - rom.write_bytes(0x9AED2, [0xA9, 0x1F]) - rom.write_byte(0x9AEE1, 0x08) - - if world.options.virtual_console in [2, 3]: - # Hyper Zone BB colors - rom.write_bytes(0x2C5E16, [0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ]) - rom.write_bytes(0x2C8217, [0xFF, 0x1E, ]) - - # boss requirements - rom.write_bytes(0x3D000, struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1], - world.boss_requirements[2], world.boss_requirements[3], - world.boss_requirements[4])) - rom.write_bytes(0x3D00A, struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF)) - rom.write_byte(0x3D00C, world.options.goal_speed.value) - rom.write_byte(0x3D00E, world.options.open_world.value) - rom.write_byte(0x3D010, world.options.death_link.value) - rom.write_byte(0x3D012, world.options.goal.value) - rom.write_byte(0x3D014, world.options.stage_shuffle.value) - rom.write_byte(0x3D016, world.options.ow_boss_requirement.value) - rom.write_byte(0x3D018, world.options.consumables.value) - rom.write_byte(0x3D01A, world.options.starsanity.value) - rom.write_byte(0x3D01C, world.options.gifting.value if world.multiworld.players > 1 else 0) - rom.write_byte(0x3D01E, world.options.strict_bosses.value) - # don't write gifting for solo game, since there's no one to send anything to - - for level in world.player_levels: - for i in range(len(world.player_levels[level])): - rom.write_bytes(0x3F002E + ((level - 1) * 14) + (i * 2), - struct.pack("H", level_pointers[world.player_levels[level][i]])) - rom.write_bytes(0x3D020 + (level - 1) * 14 + (i * 2), - struct.pack("H", world.player_levels[level][i] & 0x00FFFF)) - if (i == 0) or (i > 0 and i % 6 != 0): - rom.write_bytes(0x3D080 + (level - 1) * 12 + (i * 2), - struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6)) - - for i in range(6): - if world.boss_butch_bosses[i]: - rom.write_bytes(0x3F0000 + (level_pointers[0x770200 + i]), struct.pack("I", bb_bosses[0x770200 + i])) - - # copy ability shuffle - if world.options.copy_ability_randomization.value > 0: - for enemy in world.copy_abilities: - if enemy in miniboss_remap: - rom.write_bytes(0xB417E + (miniboss_remap[enemy] << 1), - struct.pack("H", ability_remap[world.copy_abilities[enemy]])) - else: - rom.write_bytes(0xB3CAC + (enemy_remap[enemy] << 1), - struct.pack("H", ability_remap[world.copy_abilities[enemy]])) - # following only needs done on non-door rando - # incredibly lucky this follows the same order (including 5E == star block) - rom.write_byte(0x2F77EA, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)) - rom.write_byte(0x2F7811, 0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)) - rom.write_byte(0x2F9BC4, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)) - rom.write_byte(0x2F9BEB, 0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)) - rom.write_byte(0x2FAC06, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)) - rom.write_byte(0x2FAC2D, 0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)) - rom.write_byte(0x2F9E7B, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)) - rom.write_byte(0x2F9EA2, 0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)) - rom.write_byte(0x2FA951, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)) - rom.write_byte(0x2FA978, 0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)) - rom.write_byte(0x2FA132, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)) - rom.write_byte(0x2FA159, 0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)) - rom.write_byte(0x2FA3E8, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)) - rom.write_byte(0x2FA40F, 0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)) - rom.write_byte(0x2F90E2, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)) - rom.write_byte(0x2F9109, 0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)) - - if world.options.copy_ability_randomization == 2: - for enemy in enemy_remap: - # we just won't include it for minibosses - rom.write_bytes(0xB3E40 + (enemy_remap[enemy] << 1), struct.pack("h", world.random.randint(-1, 2))) - - # write jumping goal - rom.write_bytes(0x94F8, struct.pack("H", world.options.jumping_target)) - rom.write_bytes(0x944E, struct.pack("H", world.options.jumping_target)) - - from Utils import __version__ - rom.name = bytearray( - f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] - rom.name.extend([0] * (21 - len(rom.name))) - rom.write_bytes(0x3C000, rom.name) - rom.write_byte(0x3C020, world.options.game_language.value) - - # handle palette - if world.options.kirby_flavor_preset.value != 0: - for addr in kirby_target_palettes: - target = kirby_target_palettes[addr] - palette = get_kirby_palette(world) - rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2])) - - if world.options.gooey_flavor_preset.value != 0: - for addr in gooey_target_palettes: - target = gooey_target_palettes[addr] - palette = get_gooey_palette(world) - rom.write_bytes(addr, get_palette_bytes(palette, target[0], target[1], target[2])) - - -def get_base_rom_bytes() -> bytes: - rom_file: str = get_base_rom_path() - base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) - if not base_rom_bytes: - base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb"))) - - basemd5 = hashlib.md5() - basemd5.update(base_rom_bytes) - if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}: - raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. " - "Get the correct game and version, then dump it") - get_base_rom_bytes.base_rom_bytes = base_rom_bytes - return base_rom_bytes - - -def get_base_rom_path(file_name: str = "") -> str: - options: settings.Settings = settings.get_settings() - if not file_name: - file_name = options["kdl3_options"]["rom_file"] - if not os.path.exists(file_name): - file_name = Utils.user_path(file_name) - return file_name diff --git a/worlds/kdl3/Room.py b/worlds/kdl3/Room.py deleted file mode 100644 index 256955b924ab..000000000000 --- a/worlds/kdl3/Room.py +++ /dev/null @@ -1,95 +0,0 @@ -import struct -import typing -from BaseClasses import Region, ItemClassification - -if typing.TYPE_CHECKING: - from .Rom import RomData - -animal_map = { - "Rick Spawn": 0, - "Kine Spawn": 1, - "Coo Spawn": 2, - "Nago Spawn": 3, - "ChuChu Spawn": 4, - "Pitch Spawn": 5 -} - - -class KDL3Room(Region): - pointer: int = 0 - level: int = 0 - stage: int = 0 - room: int = 0 - music: int = 0 - default_exits: typing.List[typing.Dict[str, typing.Union[int, typing.List[str]]]] - animal_pointers: typing.List[int] - enemies: typing.List[str] - entity_load: typing.List[typing.List[int]] - consumables: typing.List[typing.Dict[str, typing.Union[int, str]]] - - def __init__(self, name, player, multiworld, hint, level, stage, room, pointer, music, default_exits, - animal_pointers, enemies, entity_load, consumables, consumable_pointer): - super().__init__(name, player, multiworld, hint) - self.level = level - self.stage = stage - self.room = room - self.pointer = pointer - self.music = music - self.default_exits = default_exits - self.animal_pointers = animal_pointers - self.enemies = enemies - self.entity_load = entity_load - self.consumables = consumables - self.consumable_pointer = consumable_pointer - - def patch(self, rom: "RomData"): - rom.write_byte(self.pointer + 2, self.music) - animals = [x.item.name for x in self.locations if "Animal" in x.name] - if len(animals) > 0: - for current_animal, address in zip(animals, self.animal_pointers): - rom.write_byte(self.pointer + address + 7, animal_map[current_animal]) - if self.multiworld.worlds[self.player].options.consumables: - load_len = len(self.entity_load) - for consumable in self.consumables: - location = next(x for x in self.locations if x.name == consumable["name"]) - assert location.item - is_progression = location.item.classification & ItemClassification.progression - if load_len == 8: - # edge case, there is exactly 1 room with 8 entities and only 1 consumable among them - if not (any(x in self.entity_load for x in [[0, 22], [1, 22]]) - and any(x in self.entity_load for x in [[2, 22], [3, 22]])): - replacement_target = self.entity_load.index( - next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]])) - if is_progression: - vtype = 0 - else: - vtype = 2 - rom.write_byte(self.pointer + 88 + (replacement_target * 2), vtype) - self.entity_load[replacement_target] = [vtype, 22] - else: - if is_progression: - # we need to see if 1-ups are in our load list - if any(x not in self.entity_load for x in [[0, 22], [1, 22]]): - self.entity_load.append([0, 22]) - else: - if any(x not in self.entity_load for x in [[2, 22], [3, 22]]): - # edge case: if (1, 22) is in, we need to load (3, 22) instead - if [1, 22] in self.entity_load: - self.entity_load.append([3, 22]) - else: - self.entity_load.append([2, 22]) - if load_len < len(self.entity_load): - rom.write_bytes(self.pointer + 88 + (load_len * 2), bytes(self.entity_load[load_len])) - rom.write_bytes(self.pointer + 104 + (load_len * 2), - bytes(struct.pack("H", self.consumable_pointer))) - if is_progression: - if [1, 22] in self.entity_load: - vtype = 1 - else: - vtype = 0 - else: - if [3, 22] in self.entity_load: - vtype = 3 - else: - vtype = 2 - rom.write_byte(self.pointer + consumable["pointer"] + 7, vtype) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index 8c9f3cc46a4e..f01c82dd16a3 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -1,25 +1,25 @@ import logging -import typing -from BaseClasses import Tutorial, ItemClassification, MultiWorld +from BaseClasses import Tutorial, ItemClassification, MultiWorld, CollectionState, Item from Fill import fill_restrictive from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld -from .Items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ - trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights -from .Locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations -from .Names.AnimalFriendSpawns import animal_friend_spawns -from .Names.EnemyAbilities import vanilla_enemies, enemy_mapping, enemy_restrictive -from .Regions import create_levels, default_levels -from .Options import KDL3Options -from .Presets import kdl3_options_presets -from .Names import LocationName -from .Room import KDL3Room -from .Rules import set_rules -from .Rom import KDL3DeltaPatch, get_base_rom_path, RomData, patch_rom, KDL3JHASH, KDL3UHASH -from .Client import KDL3SNIClient - -from typing import Dict, TextIO, Optional, List +from .items import item_table, item_names, copy_ability_table, animal_friend_table, filler_item_weights, KDL3Item, \ + trap_item_table, copy_ability_access_table, star_item_weights, total_filler_weights, animal_friend_spawn_table,\ + lookup_item_to_id +from .locations import location_table, KDL3Location, level_consumables, consumable_locations, star_locations +from .names.animal_friend_spawns import animal_friend_spawns, problematic_sets +from .names.enemy_abilities import vanilla_enemies, enemy_mapping, enemy_restrictive +from .regions import create_levels, default_levels +from .options import KDL3Options, kdl3_option_groups +from .presets import kdl3_options_presets +from .names import location_name +from .room import KDL3Room +from .rules import set_rules +from .rom import KDL3ProcedurePatch, get_base_rom_path, patch_rom, KDL3JHASH, KDL3UHASH +from .client import KDL3SNIClient + +from typing import Dict, TextIO, Optional, List, Any, Mapping, ClassVar, Type import os import math import threading @@ -53,6 +53,7 @@ class KDL3WebWorld(WebWorld): ) ] options_presets = kdl3_options_presets + option_groups = kdl3_option_groups class KDL3World(World): @@ -61,35 +62,35 @@ class KDL3World(World): """ game = "Kirby's Dream Land 3" - options_dataclass: typing.ClassVar[typing.Type[PerGameCommonOptions]] = KDL3Options + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = KDL3Options options: KDL3Options - item_name_to_id = {item: item_table[item].code for item in item_table} + item_name_to_id = lookup_item_to_id location_name_to_id = {location_table[location]: location for location in location_table} item_name_groups = item_names web = KDL3WebWorld() - settings: typing.ClassVar[KDL3Settings] + settings: ClassVar[KDL3Settings] def __init__(self, multiworld: MultiWorld, player: int): - self.rom_name = None + self.rom_name: bytes = bytes() self.rom_name_available_event = threading.Event() super().__init__(multiworld, player) self.copy_abilities: Dict[str, str] = vanilla_enemies.copy() self.required_heart_stars: int = 0 # we fill this during create_items - self.boss_requirements: Dict[int, int] = dict() + self.boss_requirements: List[int] = [] self.player_levels = default_levels.copy() self.stage_shuffle_enabled = False - self.boss_butch_bosses: List[Optional[bool]] = list() - self.rooms: Optional[List[KDL3Room]] = None - - @classmethod - def stage_assert_generate(cls, multiworld: MultiWorld) -> None: - rom_file: str = get_base_rom_path() - if not os.path.exists(rom_file): - raise FileNotFoundError(f"Could not find base ROM for {cls.game}: {rom_file}") + self.boss_butch_bosses: List[Optional[bool]] = [] + self.rooms: List[KDL3Room] = [] create_regions = create_levels - def create_item(self, name: str, force_non_progression=False) -> KDL3Item: + def generate_early(self) -> None: + if self.options.total_heart_stars != -1: + logger.warning(f"Kirby's Dream Land 3 ({self.player_name}): Use of \"total_heart_stars\" is deprecated. " + f"Please use \"max_heart_stars\" instead.") + self.options.max_heart_stars.value = self.options.total_heart_stars.value + + def create_item(self, name: str, force_non_progression: bool = False) -> KDL3Item: item = item_table[name] classification = ItemClassification.filler if item.progression and not force_non_progression: @@ -99,7 +100,7 @@ def create_item(self, name: str, force_non_progression=False) -> KDL3Item: classification = ItemClassification.trap return KDL3Item(name, classification, item.code, self.player) - def get_filler_item_name(self, include_stars=True) -> str: + def get_filler_item_name(self, include_stars: bool = True) -> str: if include_stars: return self.random.choices(list(total_filler_weights.keys()), weights=list(total_filler_weights.values()))[0] @@ -112,8 +113,8 @@ def get_trap_item_name(self) -> str: self.options.slow_trap_weight.value, self.options.ability_trap_weight.value])[0] - def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: typing.List[str], - level: int, stage: int): + def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_set: List[str], + level: int, stage: int) -> Optional[str]: valid_rooms = [room for room in self.rooms if (room.level < level) or (room.level == level and room.stage < stage)] # leave out the stage in question to avoid edge valid_enemies = set() @@ -124,6 +125,10 @@ def get_restrictive_copy_ability_placement(self, copy_ability: str, enemies_to_s return None # a valid enemy got placed by a more restrictive placement return self.random.choice(sorted([enemy for enemy in valid_enemies if enemy not in placed_enemies])) + def get_pre_fill_items(self) -> List[Item]: + return [self.create_item(item) + for item in [*copy_ability_access_table.keys(), *animal_friend_spawn_table.keys()]] + def pre_fill(self) -> None: if self.options.copy_ability_randomization: # randomize copy abilities @@ -196,21 +201,40 @@ def pre_fill(self) -> None: 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 = [self.create_item(animal) for animal in animal_pool] - allstate = self.multiworld.get_all_state(False) + items: List[Item] = [self.create_item(animal) for animal in animal_pool] + allstate = CollectionState(self.multiworld) + for item in [*copy_ability_table, *animal_friend_table, *["Heart Star" for _ in range(99)]]: + self.collect(allstate, self.create_item(item)) self.random.shuffle(locations) fill_restrictive(self.multiworld, allstate, locations, items, True, True) + + # Need to ensure all of these are unique items, and replace them if they aren't + for spawns in problematic_sets: + placed = [self.get_location(spawn).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + if len(placed_names) != len(placed): + # have a duplicate + animals = [] + for spawn in spawns: + spawn_location = self.get_location(spawn) + if spawn_location.item.name not in animals: + animals.append(spawn_location.item.name) + else: + new_animal = self.random.choice([x for x in ["Rick Spawn", "Coo Spawn", "Kine Spawn", + "ChuChu Spawn", "Nago Spawn", "Pitch Spawn"] + if x not in placed_names and x not in animals]) + spawn_location.item = None + spawn_location.place_locked_item(self.create_item(new_animal)) + animals.append(new_animal) + # logically, this should be sound pre-ER. May need to adjust around it with ER in the future else: animal_friends = animal_friend_spawns.copy() for animal in animal_friends: @@ -225,21 +249,20 @@ def create_items(self) -> None: remaining_items = len(location_table) - len(itempool) if not self.options.consumables: remaining_items -= len(consumable_locations) - remaining_items -= len(star_locations) - if self.options.starsanity: - # star fill, keep consumable pool locked to consumable and fill 767 stars specifically - star_items = list(star_item_weights.keys()) - star_weights = list(star_item_weights.values()) - itempool.extend([self.create_item(item) for item in self.random.choices(star_items, weights=star_weights, - k=767)]) - total_heart_stars = self.options.total_heart_stars + if not self.options.starsanity: + remaining_items -= len(star_locations) + max_heart_stars = self.options.max_heart_stars.value + if max_heart_stars > remaining_items: + max_heart_stars = remaining_items # ensure at least 1 heart star required per world - required_heart_stars = max(int(total_heart_stars * required_percentage), 5) - filler_items = total_heart_stars - required_heart_stars - filler_amount = math.floor(filler_items * (self.options.filler_percentage / 100.0)) - trap_amount = math.floor(filler_amount * (self.options.trap_percentage / 100.0)) - filler_amount -= trap_amount - non_required_heart_stars = filler_items - filler_amount - trap_amount + required_heart_stars = min(max(int(max_heart_stars * required_percentage), 5), 99) + filler_items = remaining_items - required_heart_stars + converted_heart_stars = math.floor((max_heart_stars - required_heart_stars) * (self.options.filler_percentage / 100.0)) + non_required_heart_stars = max_heart_stars - converted_heart_stars - required_heart_stars + filler_items -= non_required_heart_stars + trap_amount = math.floor(filler_items * (self.options.trap_percentage / 100.0)) + + filler_items -= trap_amount self.required_heart_stars = required_heart_stars # handle boss requirements here requirements = [required_heart_stars] @@ -261,8 +284,8 @@ def create_items(self) -> None: requirements.insert(i - 1, quotient * i) self.boss_requirements = requirements itempool.extend([self.create_item("Heart Star") for _ in range(required_heart_stars)]) - itempool.extend([self.create_item(self.get_filler_item_name(False)) - for _ in range(filler_amount + (remaining_items - total_heart_stars))]) + itempool.extend([self.create_item(self.get_filler_item_name(bool(self.options.starsanity.value))) + for _ in range(filler_items)]) itempool.extend([self.create_item(self.get_trap_item_name()) for _ in range(trap_amount)]) itempool.extend([self.create_item("Heart Star", True) for _ in range(non_required_heart_stars)]) @@ -273,15 +296,15 @@ def create_items(self) -> None: self.multiworld.get_location(location_table[self.player_levels[level][stage]] .replace("Complete", "Stage Completion"), self.player) \ .place_locked_item(KDL3Item( - f"{LocationName.level_names_inverse[level]} - Stage Completion", + f"{location_name.level_names_inverse[level]} - Stage Completion", ItemClassification.progression, None, self.player)) set_rules = set_rules def generate_basic(self) -> None: self.stage_shuffle_enabled = self.options.stage_shuffle > 0 - goal = self.options.goal - goal_location = self.multiworld.get_location(LocationName.goals[goal], self.player) + goal = self.options.goal.value + goal_location = self.multiworld.get_location(location_name.goals[goal], self.player) goal_location.place_locked_item(KDL3Item("Love-Love Rod", ItemClassification.progression, None, self.player)) for level in range(1, 6): self.multiworld.get_location(f"Level {level} Boss - Defeated", self.player) \ @@ -300,60 +323,65 @@ def generate_basic(self) -> None: else: self.boss_butch_bosses = [False for _ in range(6)] - def generate_output(self, output_directory: str): - rom_path = "" + def generate_output(self, output_directory: str) -> None: try: - rom = RomData(get_base_rom_path()) - patch_rom(self, rom) + patch = KDL3ProcedurePatch() + patch_rom(self, patch) - rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") - rom.write_to_file(rom_path) - self.rom_name = rom.name + self.rom_name = patch.name - patch = KDL3DeltaPatch(os.path.splitext(rom_path)[0] + KDL3DeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rom_path) - patch.write() + patch.write(os.path.join(output_directory, + f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) except Exception: raise finally: self.rom_name_available_event.set() # make sure threading continues and errors are collected - if os.path.exists(rom_path): - os.unlink(rom_path) - def modify_multidata(self, multidata: dict): + def modify_multidata(self, multidata: Dict[str, Any]) -> None: # wait for self.rom_name to be available. self.rom_name_available_event.wait() + assert isinstance(self.rom_name, bytes) rom_name = getattr(self, "rom_name", None) # we skip in case of error, so that the original error in the output thread is the one that gets raised if rom_name: - new_name = base64.b64encode(bytes(self.rom_name)).decode() + new_name = base64.b64encode(self.rom_name).decode() multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + def fill_slot_data(self) -> Mapping[str, Any]: + # UT support + return {"player_levels": self.player_levels} + + def interpret_slot_data(self, slot_data: Mapping[str, Any]): + # UT support + player_levels = {int(key): value for key, value in slot_data["player_levels"].items()} + return {"player_levels": player_levels} + def write_spoiler(self, spoiler_handle: TextIO) -> None: if self.stage_shuffle_enabled: spoiler_handle.write(f"\nLevel Layout ({self.multiworld.get_player_name(self.player)}):\n") - for level in LocationName.level_names: - for stage, i in zip(self.player_levels[LocationName.level_names[level]], range(1, 7)): + for level in location_name.level_names: + for stage, i in zip(self.player_levels[location_name.level_names[level]], range(1, 7)): spoiler_handle.write(f"{level} {i}: {location_table[stage].replace(' - Complete', '')}\n") if self.options.animal_randomization: spoiler_handle.write(f"\nAnimal Friends ({self.multiworld.get_player_name(self.player)}):\n") - for level in self.player_levels: + for lvl in self.player_levels: for stage in range(6): - rooms = [room for room in self.rooms if room.level == level and room.stage == stage] + rooms = [room for room in self.rooms if room.level == lvl and room.stage == stage] animals = [] for room in rooms: animals.extend([location.item.name.replace(" Spawn", "") - for location in room.locations if "Animal" in location.name]) - spoiler_handle.write(f"{location_table[self.player_levels[level][stage]].replace(' - Complete','')}" + for location in room.locations if "Animal" in location.name + and location.item is not None]) + spoiler_handle.write(f"{location_table[self.player_levels[lvl][stage]].replace(' - Complete','')}" f": {', '.join(animals)}\n") if self.options.copy_ability_randomization: spoiler_handle.write(f"\nCopy Abilities ({self.multiworld.get_player_name(self.player)}):\n") for enemy in self.copy_abilities: spoiler_handle.write(f"{enemy}: {self.copy_abilities[enemy].replace('No Ability', 'None').replace(' Ability', '')}\n") - def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.stage_shuffle_enabled: - regions = {LocationName.level_names[level]: level for level in LocationName.level_names} + regions = {location_name.level_names[level]: level for level in location_name.level_names} level_hint_data = {} for level in regions: for stage in range(7): @@ -361,6 +389,6 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): self.player).name.replace(" - Complete", "") stage_regions = [room for room in self.rooms if stage_name in room.name] for region in stage_regions: - for location in [location for location in region.locations if location.address]: + for location in [location for location in list(region.get_locations()) if location.address]: level_hint_data[location.address] = f"{regions[level]} {stage + 1 if stage < 6 else 'Boss'}" hint_data[self.player] = level_hint_data diff --git a/worlds/kdl3/Aesthetics.py b/worlds/kdl3/aesthetics.py similarity index 91% rename from worlds/kdl3/Aesthetics.py rename to worlds/kdl3/aesthetics.py index 8c7363908f52..8b798ff93ede 100644 --- a/worlds/kdl3/Aesthetics.py +++ b/worlds/kdl3/aesthetics.py @@ -1,5 +1,9 @@ import struct -from .Options import KirbyFlavorPreset, GooeyFlavorPreset +from .options import KirbyFlavorPreset, GooeyFlavorPreset +from typing import TYPE_CHECKING, Optional, Dict, List, Tuple + +if TYPE_CHECKING: + from . import KDL3World kirby_flavor_presets = { 1: { @@ -223,6 +227,23 @@ "14": "E6E6FA", "15": "976FBD", }, + 14: { + "1": "373B3E", + "2": "98d5d3", + "3": "1aa5ab", + "4": "168f95", + "5": "4f5559", + "6": "1dbac2", + "7": "137a7f", + "8": "093a3c", + "9": "86cecb", + "10": "a0afbc", + "11": "62bfbb", + "12": "50b8b4", + "13": "bec8d1", + "14": "bce4e2", + "15": "91a2b1", + } } gooey_flavor_presets = { @@ -398,21 +419,21 @@ } -def get_kirby_palette(world): +def get_kirby_palette(world: "KDL3World") -> Optional[Dict[str, str]]: palette = world.options.kirby_flavor_preset.value if palette == KirbyFlavorPreset.option_custom: return world.options.kirby_flavor.value return kirby_flavor_presets.get(palette, None) -def get_gooey_palette(world): +def get_gooey_palette(world: "KDL3World") -> Optional[Dict[str, str]]: palette = world.options.gooey_flavor_preset.value if palette == GooeyFlavorPreset.option_custom: return world.options.gooey_flavor.value return gooey_flavor_presets.get(palette, None) -def rgb888_to_bgr555(red, green, blue) -> bytes: +def rgb888_to_bgr555(red: int, green: int, blue: int) -> bytes: red = red >> 3 green = green >> 3 blue = blue >> 3 @@ -420,15 +441,15 @@ def rgb888_to_bgr555(red, green, blue) -> bytes: return struct.pack("H", outcol) -def get_palette_bytes(palette, target, offset, factor): +def get_palette_bytes(palette: Dict[str, str], target: List[str], offset: int, factor: float) -> bytes: output_data = bytearray() for color in target: hexcol = palette[color] if hexcol.startswith("#"): hexcol = hexcol.replace("#", "") colint = int(hexcol, 16) - col = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) + col: Tuple[int, ...] = ((colint & 0xFF0000) >> 16, (colint & 0xFF00) >> 8, colint & 0xFF) col = tuple(int(int(factor*x) + offset) for x in col) byte_data = rgb888_to_bgr555(col[0], col[1], col[2]) output_data.extend(bytearray(byte_data)) - return output_data + return bytes(output_data) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/client.py similarity index 90% rename from worlds/kdl3/Client.py rename to worlds/kdl3/client.py index 1ca21d550e67..97bf68cbd99a 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/client.py @@ -11,13 +11,13 @@ from NetUtils import ClientStatus, color from Utils import async_start from worlds.AutoSNIClient import SNIClient -from .Locations import boss_locations -from .Gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes -from .ClientAddrs import consumable_addrs, star_addrs +from .locations import boss_locations +from .gifting import kdl3_gifting_options, kdl3_trap_gifts, kdl3_gifts, update_object, pop_object, initialize_giftboxes +from .client_addrs import consumable_addrs, star_addrs from typing import TYPE_CHECKING if TYPE_CHECKING: - from SNIClient import SNIClientCommandProcessor + from SNIClient import SNIClientCommandProcessor, SNIContext snes_logger = logging.getLogger("SNES") @@ -81,17 +81,16 @@ @mark_raw -def cmd_gift(self: "SNIClientCommandProcessor"): +def cmd_gift(self: "SNIClientCommandProcessor") -> None: """Toggles gifting for the current game.""" - if not getattr(self.ctx, "gifting", None): - self.ctx.gifting = True - else: - self.ctx.gifting = not self.ctx.gifting - self.output(f"Gifting set to {self.ctx.gifting}") + handler = self.ctx.client_handler + assert isinstance(handler, KDL3SNIClient) + handler.gifting = not handler.gifting + self.output(f"Gifting set to {handler.gifting}") async_start(update_object(self.ctx, f"Giftboxes;{self.ctx.team}", { f"{self.ctx.slot}": { - "IsOpen": self.ctx.gifting, + "IsOpen": handler.gifting, **kdl3_gifting_options } })) @@ -100,16 +99,17 @@ def cmd_gift(self: "SNIClientCommandProcessor"): class KDL3SNIClient(SNIClient): game = "Kirby's Dream Land 3" patch_suffix = ".apkdl3" - levels = None - consumables = None - stars = None - item_queue: typing.List = [] - initialize_gifting = False + levels: typing.Dict[int, typing.List[int]] = {} + consumables: typing.Optional[bool] = None + stars: typing.Optional[bool] = None + item_queue: typing.List[int] = [] + initialize_gifting: bool = False + gifting: bool = False giftbox_key: str = "" motherbox_key: str = "" client_random: random.Random = random.Random() - async def deathlink_kill_player(self, ctx) -> None: + async def deathlink_kill_player(self, ctx: "SNIContext") -> None: from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read game_state = await snes_read(ctx, KDL3_GAME_STATE, 1) if game_state[0] == 0xFF: @@ -131,7 +131,7 @@ async def deathlink_kill_player(self, ctx) -> None: ctx.death_state = DeathState.dead ctx.last_death_link = time.time() - async def validate_rom(self, ctx) -> bool: + async def validate_rom(self, ctx: "SNIContext") -> bool: from SNIClient import snes_read rom_name = await snes_read(ctx, KDL3_ROMNAME, 0x15) if rom_name is None or rom_name == bytes([0] * 0x15) or rom_name[:4] != b"KDL3": @@ -141,7 +141,7 @@ async def validate_rom(self, ctx) -> bool: ctx.game = self.game ctx.rom = rom_name - ctx.items_handling = 0b111 # always remote items + ctx.items_handling = 0b101 # default local items with remote start inventory ctx.allow_collect = True if "gift" not in ctx.command_processor.commands: ctx.command_processor.commands["gift"] = cmd_gift @@ -149,9 +149,10 @@ async def validate_rom(self, ctx) -> bool: death_link = await snes_read(ctx, KDL3_DEATH_LINK_ADDR, 1) if death_link: await ctx.update_death_link(bool(death_link[0] & 0b1)) + ctx.items_handling |= (death_link[0] & 0b10) # set local items if enabled return True - async def pop_item(self, ctx, in_stage): + async def pop_item(self, ctx: "SNIContext", in_stage: bool) -> None: from SNIClient import snes_buffered_write, snes_read if len(self.item_queue) > 0: item = self.item_queue.pop() @@ -168,8 +169,8 @@ async def pop_item(self, ctx, in_stage): else: self.item_queue.append(item) # no more slots, get it next go around - async def pop_gift(self, ctx): - if ctx.stored_data[self.giftbox_key]: + async def pop_gift(self, ctx: "SNIContext") -> None: + if self.giftbox_key in ctx.stored_data and ctx.stored_data[self.giftbox_key]: from SNIClient import snes_read, snes_buffered_write key, gift = ctx.stored_data[self.giftbox_key].popitem() await pop_object(ctx, self.giftbox_key, key) @@ -214,7 +215,7 @@ async def pop_gift(self, ctx): quality = min(10, quality * 2) else: # it's not really edible, but he'll eat it anyway - quality = self.client_random.choices(range(0, 2), {0: 75, 1: 25})[0] + quality = self.client_random.choices(range(0, 2), [75, 25])[0] kirby_hp = await snes_read(ctx, KDL3_KIRBY_HP, 1) gooey_hp = await snes_read(ctx, KDL3_KIRBY_HP + 2, 1) snes_buffered_write(ctx, KDL3_SOUND_FX, bytes([0x26])) @@ -224,7 +225,8 @@ async def pop_gift(self, ctx): else: snes_buffered_write(ctx, KDL3_KIRBY_HP, struct.pack("H", min(kirby_hp[0] + quality, 10))) - async def pick_gift_recipient(self, ctx, gift): + async def pick_gift_recipient(self, ctx: "SNIContext", gift: int) -> None: + assert ctx.slot if gift != 4: gift_base = kdl3_gifts[gift] else: @@ -238,7 +240,7 @@ async def pick_gift_recipient(self, ctx, gift): if desire > most_applicable: most_applicable = desire most_applicable_slot = int(slot) - elif most_applicable_slot == ctx.slot and info["AcceptsAnyGift"]: + elif most_applicable_slot != ctx.slot and most_applicable == -1 and info["AcceptsAnyGift"]: # only send to ourselves if no one else will take it most_applicable_slot = int(slot) # print(most_applicable, most_applicable_slot) @@ -257,7 +259,7 @@ async def pick_gift_recipient(self, ctx, gift): item_uuid: item, }) - async def game_watcher(self, ctx) -> None: + async def game_watcher(self, ctx: "SNIContext") -> None: try: from SNIClient import snes_buffered_write, snes_flush_writes, snes_read rom = await snes_read(ctx, KDL3_ROMNAME, 0x15) @@ -278,11 +280,12 @@ async def game_watcher(self, ctx) -> None: await initialize_giftboxes(ctx, self.giftbox_key, self.motherbox_key, bool(enable_gifting[0])) self.initialize_gifting = True # can't check debug anymore, without going and copying the value. might be important later. - if self.levels is None: + if not self.levels: self.levels = dict() for i in range(5): level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) - self.levels[i] = unpack("HHHHHHH", level_data) + self.levels[i] = [int.from_bytes(level_data[idx:idx+1], "little") + for idx in range(0, len(level_data), 2)] self.levels[5] = [0x0205, # Hyper Zone 0, # MG-5, can't send from here 0x0300, # Boss Butch @@ -371,7 +374,7 @@ async def game_watcher(self, ctx) -> None: stages_raw = await snes_read(ctx, KDL3_COMPLETED_STAGES, 60) stages = struct.unpack("HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", stages_raw) for i in range(30): - loc_id = 0x770000 + i + 1 + loc_id = 0x770000 + i if stages[i] == 1 and loc_id not in ctx.checked_locations: new_checks.append(loc_id) elif loc_id in ctx.checked_locations: @@ -381,8 +384,8 @@ async def game_watcher(self, ctx) -> None: heart_stars = await snes_read(ctx, KDL3_HEART_STARS, 35) for i in range(5): start_ind = i * 7 - for j in range(1, 7): - level_ind = start_ind + j - 1 + for j in range(6): + level_ind = start_ind + j loc_id = 0x770100 + (6 * i) + j if heart_stars[level_ind] and loc_id not in ctx.checked_locations: new_checks.append(loc_id) @@ -401,6 +404,9 @@ async def game_watcher(self, ctx) -> None: if star not in ctx.checked_locations and stars[star_addrs[star]] == 0x01: new_checks.append(star) + if not game_state: + return + if game_state[0] != 0xFF: await self.pop_gift(ctx) await self.pop_item(ctx, game_state[0] != 0xFF) @@ -408,7 +414,7 @@ async def game_watcher(self, ctx) -> None: # boss status boss_flag_bytes = await snes_read(ctx, KDL3_BOSS_STATUS, 2) - boss_flag = unpack("H", boss_flag_bytes)[0] + boss_flag = int.from_bytes(boss_flag_bytes, "little") for bitmask, boss in zip(range(1, 11, 2), boss_locations.keys()): if boss_flag & (1 << bitmask) > 0 and boss not in ctx.checked_locations: new_checks.append(boss) diff --git a/worlds/kdl3/ClientAddrs.py b/worlds/kdl3/client_addrs.py similarity index 100% rename from worlds/kdl3/ClientAddrs.py rename to worlds/kdl3/client_addrs.py diff --git a/worlds/kdl3/Compression.py b/worlds/kdl3/compression.py similarity index 100% rename from worlds/kdl3/Compression.py rename to worlds/kdl3/compression.py diff --git a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 index cd002121cd38..3b6b338d5a92 100644 Binary files a/worlds/kdl3/data/kdl3_basepatch.bsdiff4 and b/worlds/kdl3/data/kdl3_basepatch.bsdiff4 differ diff --git a/worlds/kdl3/Gifting.py b/worlds/kdl3/gifting.py similarity index 90% rename from worlds/kdl3/Gifting.py rename to worlds/kdl3/gifting.py index 8ccba7ec1ae6..e1626091000e 100644 --- a/worlds/kdl3/Gifting.py +++ b/worlds/kdl3/gifting.py @@ -1,8 +1,11 @@ # Small subfile to handle gifting info such as desired traits and giftbox management import typing +if typing.TYPE_CHECKING: + from SNIClient import SNIContext -async def update_object(ctx, key: str, value: typing.Dict): + +async def update_object(ctx: "SNIContext", key: str, value: typing.Dict[str, typing.Any]) -> None: await ctx.send_msgs([ { "cmd": "Set", @@ -16,7 +19,7 @@ async def update_object(ctx, key: str, value: typing.Dict): ]) -async def pop_object(ctx, key: str, value: str): +async def pop_object(ctx: "SNIContext", key: str, value: str) -> None: await ctx.send_msgs([ { "cmd": "Set", @@ -30,14 +33,14 @@ async def pop_object(ctx, key: str, value: str): ]) -async def initialize_giftboxes(ctx, giftbox_key: str, motherbox_key: str, is_open: bool): +async def initialize_giftboxes(ctx: "SNIContext", giftbox_key: str, motherbox_key: str, is_open: bool) -> None: ctx.set_notify(motherbox_key, giftbox_key) await update_object(ctx, f"Giftboxes;{ctx.team}", {f"{ctx.slot}": - { - "IsOpen": is_open, - **kdl3_gifting_options - }}) - ctx.gifting = is_open + { + "IsOpen": is_open, + **kdl3_gifting_options + }}) + ctx.client_handler.gifting = is_open kdl3_gifting_options = { diff --git a/worlds/kdl3/Items.py b/worlds/kdl3/items.py similarity index 95% rename from worlds/kdl3/Items.py rename to worlds/kdl3/items.py index 66c7f8fee323..72687a6065d4 100644 --- a/worlds/kdl3/Items.py +++ b/worlds/kdl3/items.py @@ -77,9 +77,9 @@ class KDL3Item(Item): } star_item_weights = { - "Little Star": 4, - "Medium Star": 2, - "Big Star": 1 + "Little Star": 16, + "Medium Star": 8, + "Big Star": 4 } total_filler_weights = { @@ -102,4 +102,4 @@ class KDL3Item(Item): "Animal Friend": set(animal_friend_table), } -lookup_name_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} +lookup_item_to_id: typing.Dict[str, int] = {item_name: data.code for item_name, data in item_table.items() if data.code} diff --git a/worlds/kdl3/locations.py b/worlds/kdl3/locations.py new file mode 100644 index 000000000000..4fa1bfad7047 --- /dev/null +++ b/worlds/kdl3/locations.py @@ -0,0 +1,940 @@ +import typing +from BaseClasses import Location, Region +from .names import location_name + +if typing.TYPE_CHECKING: + from .room import KDL3Room + + +class KDL3Location(Location): + game: str = "Kirby's Dream Land 3" + room: typing.Optional["KDL3Room"] = None + + def __init__(self, player: int, name: str, address: typing.Optional[int], parent: typing.Union[Region, None]): + super().__init__(player, name, address, parent) + if not address: + self.show_in_spoiler = False + + +stage_locations = { + 0x770000: location_name.grass_land_1, + 0x770001: location_name.grass_land_2, + 0x770002: location_name.grass_land_3, + 0x770003: location_name.grass_land_4, + 0x770004: location_name.grass_land_5, + 0x770005: location_name.grass_land_6, + 0x770006: location_name.ripple_field_1, + 0x770007: location_name.ripple_field_2, + 0x770008: location_name.ripple_field_3, + 0x770009: location_name.ripple_field_4, + 0x77000A: location_name.ripple_field_5, + 0x77000B: location_name.ripple_field_6, + 0x77000C: location_name.sand_canyon_1, + 0x77000D: location_name.sand_canyon_2, + 0x77000E: location_name.sand_canyon_3, + 0x77000F: location_name.sand_canyon_4, + 0x770010: location_name.sand_canyon_5, + 0x770011: location_name.sand_canyon_6, + 0x770012: location_name.cloudy_park_1, + 0x770013: location_name.cloudy_park_2, + 0x770014: location_name.cloudy_park_3, + 0x770015: location_name.cloudy_park_4, + 0x770016: location_name.cloudy_park_5, + 0x770017: location_name.cloudy_park_6, + 0x770018: location_name.iceberg_1, + 0x770019: location_name.iceberg_2, + 0x77001A: location_name.iceberg_3, + 0x77001B: location_name.iceberg_4, + 0x77001C: location_name.iceberg_5, + 0x77001D: location_name.iceberg_6, +} + +heart_star_locations = { + 0x770100: location_name.grass_land_tulip, + 0x770101: location_name.grass_land_muchi, + 0x770102: location_name.grass_land_pitcherman, + 0x770103: location_name.grass_land_chao, + 0x770104: location_name.grass_land_mine, + 0x770105: location_name.grass_land_pierre, + 0x770106: location_name.ripple_field_kamuribana, + 0x770107: location_name.ripple_field_bakasa, + 0x770108: location_name.ripple_field_elieel, + 0x770109: location_name.ripple_field_toad, + 0x77010A: location_name.ripple_field_mama_pitch, + 0x77010B: location_name.ripple_field_hb002, + 0x77010C: location_name.sand_canyon_mushrooms, + 0x77010D: location_name.sand_canyon_auntie, + 0x77010E: location_name.sand_canyon_caramello, + 0x77010F: location_name.sand_canyon_hikari, + 0x770110: location_name.sand_canyon_nyupun, + 0x770111: location_name.sand_canyon_rob, + 0x770112: location_name.cloudy_park_hibanamodoki, + 0x770113: location_name.cloudy_park_piyokeko, + 0x770114: location_name.cloudy_park_mrball, + 0x770115: location_name.cloudy_park_mikarin, + 0x770116: location_name.cloudy_park_pick, + 0x770117: location_name.cloudy_park_hb007, + 0x770118: location_name.iceberg_kogoesou, + 0x770119: location_name.iceberg_samus, + 0x77011A: location_name.iceberg_kawasaki, + 0x77011B: location_name.iceberg_name, + 0x77011C: location_name.iceberg_shiro, + 0x77011D: location_name.iceberg_angel, +} + +boss_locations = { + 0x770200: location_name.grass_land_whispy, + 0x770201: location_name.ripple_field_acro, + 0x770202: location_name.sand_canyon_poncon, + 0x770203: location_name.cloudy_park_ado, + 0x770204: location_name.iceberg_dedede, +} + +consumable_locations = { + 0x770300: location_name.grass_land_1_u1, + 0x770301: location_name.grass_land_1_m1, + 0x770302: location_name.grass_land_2_u1, + 0x770303: location_name.grass_land_3_u1, + 0x770304: location_name.grass_land_3_m1, + 0x770305: location_name.grass_land_4_m1, + 0x770306: location_name.grass_land_4_u1, + 0x770307: location_name.grass_land_4_m2, + 0x770308: location_name.grass_land_4_m3, + 0x770309: location_name.grass_land_6_u1, + 0x77030A: location_name.grass_land_6_u2, + 0x77030B: location_name.ripple_field_2_u1, + 0x77030C: location_name.ripple_field_2_m1, + 0x77030D: location_name.ripple_field_3_m1, + 0x77030E: location_name.ripple_field_3_u1, + 0x77030F: location_name.ripple_field_4_m2, + 0x770310: location_name.ripple_field_4_u1, + 0x770311: location_name.ripple_field_4_m1, + 0x770312: location_name.ripple_field_5_u1, + 0x770313: location_name.ripple_field_5_m2, + 0x770314: location_name.ripple_field_5_m1, + 0x770315: location_name.sand_canyon_1_u1, + 0x770316: location_name.sand_canyon_2_u1, + 0x770317: location_name.sand_canyon_2_m1, + 0x770318: location_name.sand_canyon_4_m1, + 0x770319: location_name.sand_canyon_4_u1, + 0x77031A: location_name.sand_canyon_4_m2, + 0x77031B: location_name.sand_canyon_5_u1, + 0x77031C: location_name.sand_canyon_5_u3, + 0x77031D: location_name.sand_canyon_5_m1, + 0x77031E: location_name.sand_canyon_5_u4, + 0x77031F: location_name.sand_canyon_5_u2, + 0x770320: location_name.cloudy_park_1_m1, + 0x770321: location_name.cloudy_park_1_u1, + 0x770322: location_name.cloudy_park_4_u1, + 0x770323: location_name.cloudy_park_4_m1, + 0x770324: location_name.cloudy_park_5_m1, + 0x770325: location_name.cloudy_park_6_u1, + 0x770326: location_name.iceberg_3_m1, + 0x770327: location_name.iceberg_5_u1, + 0x770328: location_name.iceberg_5_u2, + 0x770329: location_name.iceberg_5_u3, + 0x77032A: location_name.iceberg_6_m1, + 0x77032B: location_name.iceberg_6_u1, +} + +level_consumables = { + 1: [0, 1], + 2: [2], + 3: [3, 4], + 4: [5, 6, 7, 8], + 6: [9, 10], + 8: [11, 12], + 9: [13, 14], + 10: [15, 16, 17], + 11: [18, 19, 20], + 13: [21], + 14: [22, 23], + 16: [24, 25, 26], + 17: [27, 28, 29, 30, 31], + 19: [32, 33], + 22: [34, 35], + 23: [36], + 24: [37], + 27: [38], + 29: [39, 40, 41], + 30: [42, 43], +} + +star_locations = { + 0x770401: location_name.grass_land_1_s1, + 0x770402: location_name.grass_land_1_s2, + 0x770403: location_name.grass_land_1_s3, + 0x770404: location_name.grass_land_1_s4, + 0x770405: location_name.grass_land_1_s5, + 0x770406: location_name.grass_land_1_s6, + 0x770407: location_name.grass_land_1_s7, + 0x770408: location_name.grass_land_1_s8, + 0x770409: location_name.grass_land_1_s9, + 0x77040a: location_name.grass_land_1_s10, + 0x77040b: location_name.grass_land_1_s11, + 0x77040c: location_name.grass_land_1_s12, + 0x77040d: location_name.grass_land_1_s13, + 0x77040e: location_name.grass_land_1_s14, + 0x77040f: location_name.grass_land_1_s15, + 0x770410: location_name.grass_land_1_s16, + 0x770411: location_name.grass_land_1_s17, + 0x770412: location_name.grass_land_1_s18, + 0x770413: location_name.grass_land_1_s19, + 0x770414: location_name.grass_land_1_s20, + 0x770415: location_name.grass_land_1_s21, + 0x770416: location_name.grass_land_1_s22, + 0x770417: location_name.grass_land_1_s23, + 0x770418: location_name.grass_land_2_s1, + 0x770419: location_name.grass_land_2_s2, + 0x77041a: location_name.grass_land_2_s3, + 0x77041b: location_name.grass_land_2_s4, + 0x77041c: location_name.grass_land_2_s5, + 0x77041d: location_name.grass_land_2_s6, + 0x77041e: location_name.grass_land_2_s7, + 0x77041f: location_name.grass_land_2_s8, + 0x770420: location_name.grass_land_2_s9, + 0x770421: location_name.grass_land_2_s10, + 0x770422: location_name.grass_land_2_s11, + 0x770423: location_name.grass_land_2_s12, + 0x770424: location_name.grass_land_2_s13, + 0x770425: location_name.grass_land_2_s14, + 0x770426: location_name.grass_land_2_s15, + 0x770427: location_name.grass_land_2_s16, + 0x770428: location_name.grass_land_2_s17, + 0x770429: location_name.grass_land_2_s18, + 0x77042a: location_name.grass_land_2_s19, + 0x77042b: location_name.grass_land_2_s20, + 0x77042c: location_name.grass_land_2_s21, + 0x77042d: location_name.grass_land_3_s1, + 0x77042e: location_name.grass_land_3_s2, + 0x77042f: location_name.grass_land_3_s3, + 0x770430: location_name.grass_land_3_s4, + 0x770431: location_name.grass_land_3_s5, + 0x770432: location_name.grass_land_3_s6, + 0x770433: location_name.grass_land_3_s7, + 0x770434: location_name.grass_land_3_s8, + 0x770435: location_name.grass_land_3_s9, + 0x770436: location_name.grass_land_3_s10, + 0x770437: location_name.grass_land_3_s11, + 0x770438: location_name.grass_land_3_s12, + 0x770439: location_name.grass_land_3_s13, + 0x77043a: location_name.grass_land_3_s14, + 0x77043b: location_name.grass_land_3_s15, + 0x77043c: location_name.grass_land_3_s16, + 0x77043d: location_name.grass_land_3_s17, + 0x77043e: location_name.grass_land_3_s18, + 0x77043f: location_name.grass_land_3_s19, + 0x770440: location_name.grass_land_3_s20, + 0x770441: location_name.grass_land_3_s21, + 0x770442: location_name.grass_land_3_s22, + 0x770443: location_name.grass_land_3_s23, + 0x770444: location_name.grass_land_3_s24, + 0x770445: location_name.grass_land_3_s25, + 0x770446: location_name.grass_land_3_s26, + 0x770447: location_name.grass_land_3_s27, + 0x770448: location_name.grass_land_3_s28, + 0x770449: location_name.grass_land_3_s29, + 0x77044a: location_name.grass_land_3_s30, + 0x77044b: location_name.grass_land_3_s31, + 0x77044c: location_name.grass_land_4_s1, + 0x77044d: location_name.grass_land_4_s2, + 0x77044e: location_name.grass_land_4_s3, + 0x77044f: location_name.grass_land_4_s4, + 0x770450: location_name.grass_land_4_s5, + 0x770451: location_name.grass_land_4_s6, + 0x770452: location_name.grass_land_4_s7, + 0x770453: location_name.grass_land_4_s8, + 0x770454: location_name.grass_land_4_s9, + 0x770455: location_name.grass_land_4_s10, + 0x770456: location_name.grass_land_4_s11, + 0x770457: location_name.grass_land_4_s12, + 0x770458: location_name.grass_land_4_s13, + 0x770459: location_name.grass_land_4_s14, + 0x77045a: location_name.grass_land_4_s15, + 0x77045b: location_name.grass_land_4_s16, + 0x77045c: location_name.grass_land_4_s17, + 0x77045d: location_name.grass_land_4_s18, + 0x77045e: location_name.grass_land_4_s19, + 0x77045f: location_name.grass_land_4_s20, + 0x770460: location_name.grass_land_4_s21, + 0x770461: location_name.grass_land_4_s22, + 0x770462: location_name.grass_land_4_s23, + 0x770463: location_name.grass_land_4_s24, + 0x770464: location_name.grass_land_4_s25, + 0x770465: location_name.grass_land_4_s26, + 0x770466: location_name.grass_land_4_s27, + 0x770467: location_name.grass_land_4_s28, + 0x770468: location_name.grass_land_4_s29, + 0x770469: location_name.grass_land_4_s30, + 0x77046a: location_name.grass_land_4_s31, + 0x77046b: location_name.grass_land_4_s32, + 0x77046c: location_name.grass_land_4_s33, + 0x77046d: location_name.grass_land_4_s34, + 0x77046e: location_name.grass_land_4_s35, + 0x77046f: location_name.grass_land_4_s36, + 0x770470: location_name.grass_land_4_s37, + 0x770471: location_name.grass_land_5_s1, + 0x770472: location_name.grass_land_5_s2, + 0x770473: location_name.grass_land_5_s3, + 0x770474: location_name.grass_land_5_s4, + 0x770475: location_name.grass_land_5_s5, + 0x770476: location_name.grass_land_5_s6, + 0x770477: location_name.grass_land_5_s7, + 0x770478: location_name.grass_land_5_s8, + 0x770479: location_name.grass_land_5_s9, + 0x77047a: location_name.grass_land_5_s10, + 0x77047b: location_name.grass_land_5_s11, + 0x77047c: location_name.grass_land_5_s12, + 0x77047d: location_name.grass_land_5_s13, + 0x77047e: location_name.grass_land_5_s14, + 0x77047f: location_name.grass_land_5_s15, + 0x770480: location_name.grass_land_5_s16, + 0x770481: location_name.grass_land_5_s17, + 0x770482: location_name.grass_land_5_s18, + 0x770483: location_name.grass_land_5_s19, + 0x770484: location_name.grass_land_5_s20, + 0x770485: location_name.grass_land_5_s21, + 0x770486: location_name.grass_land_5_s22, + 0x770487: location_name.grass_land_5_s23, + 0x770488: location_name.grass_land_5_s24, + 0x770489: location_name.grass_land_5_s25, + 0x77048a: location_name.grass_land_5_s26, + 0x77048b: location_name.grass_land_5_s27, + 0x77048c: location_name.grass_land_5_s28, + 0x77048d: location_name.grass_land_5_s29, + 0x77048e: location_name.grass_land_6_s1, + 0x77048f: location_name.grass_land_6_s2, + 0x770490: location_name.grass_land_6_s3, + 0x770491: location_name.grass_land_6_s4, + 0x770492: location_name.grass_land_6_s5, + 0x770493: location_name.grass_land_6_s6, + 0x770494: location_name.grass_land_6_s7, + 0x770495: location_name.grass_land_6_s8, + 0x770496: location_name.grass_land_6_s9, + 0x770497: location_name.grass_land_6_s10, + 0x770498: location_name.grass_land_6_s11, + 0x770499: location_name.grass_land_6_s12, + 0x77049a: location_name.grass_land_6_s13, + 0x77049b: location_name.grass_land_6_s14, + 0x77049c: location_name.grass_land_6_s15, + 0x77049d: location_name.grass_land_6_s16, + 0x77049e: location_name.grass_land_6_s17, + 0x77049f: location_name.grass_land_6_s18, + 0x7704a0: location_name.grass_land_6_s19, + 0x7704a1: location_name.grass_land_6_s20, + 0x7704a2: location_name.grass_land_6_s21, + 0x7704a3: location_name.grass_land_6_s22, + 0x7704a4: location_name.grass_land_6_s23, + 0x7704a5: location_name.grass_land_6_s24, + 0x7704a6: location_name.grass_land_6_s25, + 0x7704a7: location_name.grass_land_6_s26, + 0x7704a8: location_name.grass_land_6_s27, + 0x7704a9: location_name.grass_land_6_s28, + 0x7704aa: location_name.grass_land_6_s29, + 0x7704ab: location_name.ripple_field_1_s1, + 0x7704ac: location_name.ripple_field_1_s2, + 0x7704ad: location_name.ripple_field_1_s3, + 0x7704ae: location_name.ripple_field_1_s4, + 0x7704af: location_name.ripple_field_1_s5, + 0x7704b0: location_name.ripple_field_1_s6, + 0x7704b1: location_name.ripple_field_1_s7, + 0x7704b2: location_name.ripple_field_1_s8, + 0x7704b3: location_name.ripple_field_1_s9, + 0x7704b4: location_name.ripple_field_1_s10, + 0x7704b5: location_name.ripple_field_1_s11, + 0x7704b6: location_name.ripple_field_1_s12, + 0x7704b7: location_name.ripple_field_1_s13, + 0x7704b8: location_name.ripple_field_1_s14, + 0x7704b9: location_name.ripple_field_1_s15, + 0x7704ba: location_name.ripple_field_1_s16, + 0x7704bb: location_name.ripple_field_1_s17, + 0x7704bc: location_name.ripple_field_1_s18, + 0x7704bd: location_name.ripple_field_1_s19, + 0x7704be: location_name.ripple_field_2_s1, + 0x7704bf: location_name.ripple_field_2_s2, + 0x7704c0: location_name.ripple_field_2_s3, + 0x7704c1: location_name.ripple_field_2_s4, + 0x7704c2: location_name.ripple_field_2_s5, + 0x7704c3: location_name.ripple_field_2_s6, + 0x7704c4: location_name.ripple_field_2_s7, + 0x7704c5: location_name.ripple_field_2_s8, + 0x7704c6: location_name.ripple_field_2_s9, + 0x7704c7: location_name.ripple_field_2_s10, + 0x7704c8: location_name.ripple_field_2_s11, + 0x7704c9: location_name.ripple_field_2_s12, + 0x7704ca: location_name.ripple_field_2_s13, + 0x7704cb: location_name.ripple_field_2_s14, + 0x7704cc: location_name.ripple_field_2_s15, + 0x7704cd: location_name.ripple_field_2_s16, + 0x7704ce: location_name.ripple_field_2_s17, + 0x7704cf: location_name.ripple_field_3_s1, + 0x7704d0: location_name.ripple_field_3_s2, + 0x7704d1: location_name.ripple_field_3_s3, + 0x7704d2: location_name.ripple_field_3_s4, + 0x7704d3: location_name.ripple_field_3_s5, + 0x7704d4: location_name.ripple_field_3_s6, + 0x7704d5: location_name.ripple_field_3_s7, + 0x7704d6: location_name.ripple_field_3_s8, + 0x7704d7: location_name.ripple_field_3_s9, + 0x7704d8: location_name.ripple_field_3_s10, + 0x7704d9: location_name.ripple_field_3_s11, + 0x7704da: location_name.ripple_field_3_s12, + 0x7704db: location_name.ripple_field_3_s13, + 0x7704dc: location_name.ripple_field_3_s14, + 0x7704dd: location_name.ripple_field_3_s15, + 0x7704de: location_name.ripple_field_3_s16, + 0x7704df: location_name.ripple_field_3_s17, + 0x7704e0: location_name.ripple_field_3_s18, + 0x7704e1: location_name.ripple_field_3_s19, + 0x7704e2: location_name.ripple_field_3_s20, + 0x7704e3: location_name.ripple_field_3_s21, + 0x7704e4: location_name.ripple_field_4_s1, + 0x7704e5: location_name.ripple_field_4_s2, + 0x7704e6: location_name.ripple_field_4_s3, + 0x7704e7: location_name.ripple_field_4_s4, + 0x7704e8: location_name.ripple_field_4_s5, + 0x7704e9: location_name.ripple_field_4_s6, + 0x7704ea: location_name.ripple_field_4_s7, + 0x7704eb: location_name.ripple_field_4_s8, + 0x7704ec: location_name.ripple_field_4_s9, + 0x7704ed: location_name.ripple_field_4_s10, + 0x7704ee: location_name.ripple_field_4_s11, + 0x7704ef: location_name.ripple_field_4_s12, + 0x7704f0: location_name.ripple_field_4_s13, + 0x7704f1: location_name.ripple_field_4_s14, + 0x7704f2: location_name.ripple_field_4_s15, + 0x7704f3: location_name.ripple_field_4_s16, + 0x7704f4: location_name.ripple_field_4_s17, + 0x7704f5: location_name.ripple_field_4_s18, + 0x7704f6: location_name.ripple_field_4_s19, + 0x7704f7: location_name.ripple_field_4_s20, + 0x7704f8: location_name.ripple_field_4_s21, + 0x7704f9: location_name.ripple_field_4_s22, + 0x7704fa: location_name.ripple_field_4_s23, + 0x7704fb: location_name.ripple_field_4_s24, + 0x7704fc: location_name.ripple_field_4_s25, + 0x7704fd: location_name.ripple_field_4_s26, + 0x7704fe: location_name.ripple_field_4_s27, + 0x7704ff: location_name.ripple_field_4_s28, + 0x770500: location_name.ripple_field_4_s29, + 0x770501: location_name.ripple_field_4_s30, + 0x770502: location_name.ripple_field_4_s31, + 0x770503: location_name.ripple_field_4_s32, + 0x770504: location_name.ripple_field_4_s33, + 0x770505: location_name.ripple_field_4_s34, + 0x770506: location_name.ripple_field_4_s35, + 0x770507: location_name.ripple_field_4_s36, + 0x770508: location_name.ripple_field_4_s37, + 0x770509: location_name.ripple_field_4_s38, + 0x77050a: location_name.ripple_field_4_s39, + 0x77050b: location_name.ripple_field_4_s40, + 0x77050c: location_name.ripple_field_4_s41, + 0x77050d: location_name.ripple_field_4_s42, + 0x77050e: location_name.ripple_field_4_s43, + 0x77050f: location_name.ripple_field_4_s44, + 0x770510: location_name.ripple_field_4_s45, + 0x770511: location_name.ripple_field_4_s46, + 0x770512: location_name.ripple_field_4_s47, + 0x770513: location_name.ripple_field_4_s48, + 0x770514: location_name.ripple_field_4_s49, + 0x770515: location_name.ripple_field_4_s50, + 0x770516: location_name.ripple_field_4_s51, + 0x770517: location_name.ripple_field_5_s1, + 0x770518: location_name.ripple_field_5_s2, + 0x770519: location_name.ripple_field_5_s3, + 0x77051a: location_name.ripple_field_5_s4, + 0x77051b: location_name.ripple_field_5_s5, + 0x77051c: location_name.ripple_field_5_s6, + 0x77051d: location_name.ripple_field_5_s7, + 0x77051e: location_name.ripple_field_5_s8, + 0x77051f: location_name.ripple_field_5_s9, + 0x770520: location_name.ripple_field_5_s10, + 0x770521: location_name.ripple_field_5_s11, + 0x770522: location_name.ripple_field_5_s12, + 0x770523: location_name.ripple_field_5_s13, + 0x770524: location_name.ripple_field_5_s14, + 0x770525: location_name.ripple_field_5_s15, + 0x770526: location_name.ripple_field_5_s16, + 0x770527: location_name.ripple_field_5_s17, + 0x770528: location_name.ripple_field_5_s18, + 0x770529: location_name.ripple_field_5_s19, + 0x77052a: location_name.ripple_field_5_s20, + 0x77052b: location_name.ripple_field_5_s21, + 0x77052c: location_name.ripple_field_5_s22, + 0x77052d: location_name.ripple_field_5_s23, + 0x77052e: location_name.ripple_field_5_s24, + 0x77052f: location_name.ripple_field_5_s25, + 0x770530: location_name.ripple_field_5_s26, + 0x770531: location_name.ripple_field_5_s27, + 0x770532: location_name.ripple_field_5_s28, + 0x770533: location_name.ripple_field_5_s29, + 0x770534: location_name.ripple_field_5_s30, + 0x770535: location_name.ripple_field_5_s31, + 0x770536: location_name.ripple_field_5_s32, + 0x770537: location_name.ripple_field_5_s33, + 0x770538: location_name.ripple_field_5_s34, + 0x770539: location_name.ripple_field_5_s35, + 0x77053a: location_name.ripple_field_5_s36, + 0x77053b: location_name.ripple_field_5_s37, + 0x77053c: location_name.ripple_field_5_s38, + 0x77053d: location_name.ripple_field_5_s39, + 0x77053e: location_name.ripple_field_5_s40, + 0x77053f: location_name.ripple_field_5_s41, + 0x770540: location_name.ripple_field_5_s42, + 0x770541: location_name.ripple_field_5_s43, + 0x770542: location_name.ripple_field_5_s44, + 0x770543: location_name.ripple_field_5_s45, + 0x770544: location_name.ripple_field_5_s46, + 0x770545: location_name.ripple_field_5_s47, + 0x770546: location_name.ripple_field_5_s48, + 0x770547: location_name.ripple_field_5_s49, + 0x770548: location_name.ripple_field_5_s50, + 0x770549: location_name.ripple_field_5_s51, + 0x77054a: location_name.ripple_field_6_s1, + 0x77054b: location_name.ripple_field_6_s2, + 0x77054c: location_name.ripple_field_6_s3, + 0x77054d: location_name.ripple_field_6_s4, + 0x77054e: location_name.ripple_field_6_s5, + 0x77054f: location_name.ripple_field_6_s6, + 0x770550: location_name.ripple_field_6_s7, + 0x770551: location_name.ripple_field_6_s8, + 0x770552: location_name.ripple_field_6_s9, + 0x770553: location_name.ripple_field_6_s10, + 0x770554: location_name.ripple_field_6_s11, + 0x770555: location_name.ripple_field_6_s12, + 0x770556: location_name.ripple_field_6_s13, + 0x770557: location_name.ripple_field_6_s14, + 0x770558: location_name.ripple_field_6_s15, + 0x770559: location_name.ripple_field_6_s16, + 0x77055a: location_name.ripple_field_6_s17, + 0x77055b: location_name.ripple_field_6_s18, + 0x77055c: location_name.ripple_field_6_s19, + 0x77055d: location_name.ripple_field_6_s20, + 0x77055e: location_name.ripple_field_6_s21, + 0x77055f: location_name.ripple_field_6_s22, + 0x770560: location_name.ripple_field_6_s23, + 0x770561: location_name.sand_canyon_1_s1, + 0x770562: location_name.sand_canyon_1_s2, + 0x770563: location_name.sand_canyon_1_s3, + 0x770564: location_name.sand_canyon_1_s4, + 0x770565: location_name.sand_canyon_1_s5, + 0x770566: location_name.sand_canyon_1_s6, + 0x770567: location_name.sand_canyon_1_s7, + 0x770568: location_name.sand_canyon_1_s8, + 0x770569: location_name.sand_canyon_1_s9, + 0x77056a: location_name.sand_canyon_1_s10, + 0x77056b: location_name.sand_canyon_1_s11, + 0x77056c: location_name.sand_canyon_1_s12, + 0x77056d: location_name.sand_canyon_1_s13, + 0x77056e: location_name.sand_canyon_1_s14, + 0x77056f: location_name.sand_canyon_1_s15, + 0x770570: location_name.sand_canyon_1_s16, + 0x770571: location_name.sand_canyon_1_s17, + 0x770572: location_name.sand_canyon_1_s18, + 0x770573: location_name.sand_canyon_1_s19, + 0x770574: location_name.sand_canyon_1_s20, + 0x770575: location_name.sand_canyon_1_s21, + 0x770576: location_name.sand_canyon_1_s22, + 0x770577: location_name.sand_canyon_2_s1, + 0x770578: location_name.sand_canyon_2_s2, + 0x770579: location_name.sand_canyon_2_s3, + 0x77057a: location_name.sand_canyon_2_s4, + 0x77057b: location_name.sand_canyon_2_s5, + 0x77057c: location_name.sand_canyon_2_s6, + 0x77057d: location_name.sand_canyon_2_s7, + 0x77057e: location_name.sand_canyon_2_s8, + 0x77057f: location_name.sand_canyon_2_s9, + 0x770580: location_name.sand_canyon_2_s10, + 0x770581: location_name.sand_canyon_2_s11, + 0x770582: location_name.sand_canyon_2_s12, + 0x770583: location_name.sand_canyon_2_s13, + 0x770584: location_name.sand_canyon_2_s14, + 0x770585: location_name.sand_canyon_2_s15, + 0x770586: location_name.sand_canyon_2_s16, + 0x770587: location_name.sand_canyon_2_s17, + 0x770588: location_name.sand_canyon_2_s18, + 0x770589: location_name.sand_canyon_2_s19, + 0x77058a: location_name.sand_canyon_2_s20, + 0x77058b: location_name.sand_canyon_2_s21, + 0x77058c: location_name.sand_canyon_2_s22, + 0x77058d: location_name.sand_canyon_2_s23, + 0x77058e: location_name.sand_canyon_2_s24, + 0x77058f: location_name.sand_canyon_2_s25, + 0x770590: location_name.sand_canyon_2_s26, + 0x770591: location_name.sand_canyon_2_s27, + 0x770592: location_name.sand_canyon_2_s28, + 0x770593: location_name.sand_canyon_2_s29, + 0x770594: location_name.sand_canyon_2_s30, + 0x770595: location_name.sand_canyon_2_s31, + 0x770596: location_name.sand_canyon_2_s32, + 0x770597: location_name.sand_canyon_2_s33, + 0x770598: location_name.sand_canyon_2_s34, + 0x770599: location_name.sand_canyon_2_s35, + 0x77059a: location_name.sand_canyon_2_s36, + 0x77059b: location_name.sand_canyon_2_s37, + 0x77059c: location_name.sand_canyon_2_s38, + 0x77059d: location_name.sand_canyon_2_s39, + 0x77059e: location_name.sand_canyon_2_s40, + 0x77059f: location_name.sand_canyon_2_s41, + 0x7705a0: location_name.sand_canyon_2_s42, + 0x7705a1: location_name.sand_canyon_2_s43, + 0x7705a2: location_name.sand_canyon_2_s44, + 0x7705a3: location_name.sand_canyon_2_s45, + 0x7705a4: location_name.sand_canyon_2_s46, + 0x7705a5: location_name.sand_canyon_2_s47, + 0x7705a6: location_name.sand_canyon_2_s48, + 0x7705a7: location_name.sand_canyon_3_s1, + 0x7705a8: location_name.sand_canyon_3_s2, + 0x7705a9: location_name.sand_canyon_3_s3, + 0x7705aa: location_name.sand_canyon_3_s4, + 0x7705ab: location_name.sand_canyon_3_s5, + 0x7705ac: location_name.sand_canyon_3_s6, + 0x7705ad: location_name.sand_canyon_3_s7, + 0x7705ae: location_name.sand_canyon_3_s8, + 0x7705af: location_name.sand_canyon_3_s9, + 0x7705b0: location_name.sand_canyon_3_s10, + 0x7705b1: location_name.sand_canyon_4_s1, + 0x7705b2: location_name.sand_canyon_4_s2, + 0x7705b3: location_name.sand_canyon_4_s3, + 0x7705b4: location_name.sand_canyon_4_s4, + 0x7705b5: location_name.sand_canyon_4_s5, + 0x7705b6: location_name.sand_canyon_4_s6, + 0x7705b7: location_name.sand_canyon_4_s7, + 0x7705b8: location_name.sand_canyon_4_s8, + 0x7705b9: location_name.sand_canyon_4_s9, + 0x7705ba: location_name.sand_canyon_4_s10, + 0x7705bb: location_name.sand_canyon_4_s11, + 0x7705bc: location_name.sand_canyon_4_s12, + 0x7705bd: location_name.sand_canyon_4_s13, + 0x7705be: location_name.sand_canyon_4_s14, + 0x7705bf: location_name.sand_canyon_4_s15, + 0x7705c0: location_name.sand_canyon_4_s16, + 0x7705c1: location_name.sand_canyon_4_s17, + 0x7705c2: location_name.sand_canyon_4_s18, + 0x7705c3: location_name.sand_canyon_4_s19, + 0x7705c4: location_name.sand_canyon_4_s20, + 0x7705c5: location_name.sand_canyon_4_s21, + 0x7705c6: location_name.sand_canyon_4_s22, + 0x7705c7: location_name.sand_canyon_4_s23, + 0x7705c8: location_name.sand_canyon_5_s1, + 0x7705c9: location_name.sand_canyon_5_s2, + 0x7705ca: location_name.sand_canyon_5_s3, + 0x7705cb: location_name.sand_canyon_5_s4, + 0x7705cc: location_name.sand_canyon_5_s5, + 0x7705cd: location_name.sand_canyon_5_s6, + 0x7705ce: location_name.sand_canyon_5_s7, + 0x7705cf: location_name.sand_canyon_5_s8, + 0x7705d0: location_name.sand_canyon_5_s9, + 0x7705d1: location_name.sand_canyon_5_s10, + 0x7705d2: location_name.sand_canyon_5_s11, + 0x7705d3: location_name.sand_canyon_5_s12, + 0x7705d4: location_name.sand_canyon_5_s13, + 0x7705d5: location_name.sand_canyon_5_s14, + 0x7705d6: location_name.sand_canyon_5_s15, + 0x7705d7: location_name.sand_canyon_5_s16, + 0x7705d8: location_name.sand_canyon_5_s17, + 0x7705d9: location_name.sand_canyon_5_s18, + 0x7705da: location_name.sand_canyon_5_s19, + 0x7705db: location_name.sand_canyon_5_s20, + 0x7705dc: location_name.sand_canyon_5_s21, + 0x7705dd: location_name.sand_canyon_5_s22, + 0x7705de: location_name.sand_canyon_5_s23, + 0x7705df: location_name.sand_canyon_5_s24, + 0x7705e0: location_name.sand_canyon_5_s25, + 0x7705e1: location_name.sand_canyon_5_s26, + 0x7705e2: location_name.sand_canyon_5_s27, + 0x7705e3: location_name.sand_canyon_5_s28, + 0x7705e4: location_name.sand_canyon_5_s29, + 0x7705e5: location_name.sand_canyon_5_s30, + 0x7705e6: location_name.sand_canyon_5_s31, + 0x7705e7: location_name.sand_canyon_5_s32, + 0x7705e8: location_name.sand_canyon_5_s33, + 0x7705e9: location_name.sand_canyon_5_s34, + 0x7705ea: location_name.sand_canyon_5_s35, + 0x7705eb: location_name.sand_canyon_5_s36, + 0x7705ec: location_name.sand_canyon_5_s37, + 0x7705ed: location_name.sand_canyon_5_s38, + 0x7705ee: location_name.sand_canyon_5_s39, + 0x7705ef: location_name.sand_canyon_5_s40, + 0x7705f0: location_name.cloudy_park_1_s1, + 0x7705f1: location_name.cloudy_park_1_s2, + 0x7705f2: location_name.cloudy_park_1_s3, + 0x7705f3: location_name.cloudy_park_1_s4, + 0x7705f4: location_name.cloudy_park_1_s5, + 0x7705f5: location_name.cloudy_park_1_s6, + 0x7705f6: location_name.cloudy_park_1_s7, + 0x7705f7: location_name.cloudy_park_1_s8, + 0x7705f8: location_name.cloudy_park_1_s9, + 0x7705f9: location_name.cloudy_park_1_s10, + 0x7705fa: location_name.cloudy_park_1_s11, + 0x7705fb: location_name.cloudy_park_1_s12, + 0x7705fc: location_name.cloudy_park_1_s13, + 0x7705fd: location_name.cloudy_park_1_s14, + 0x7705fe: location_name.cloudy_park_1_s15, + 0x7705ff: location_name.cloudy_park_1_s16, + 0x770600: location_name.cloudy_park_1_s17, + 0x770601: location_name.cloudy_park_1_s18, + 0x770602: location_name.cloudy_park_1_s19, + 0x770603: location_name.cloudy_park_1_s20, + 0x770604: location_name.cloudy_park_1_s21, + 0x770605: location_name.cloudy_park_1_s22, + 0x770606: location_name.cloudy_park_1_s23, + 0x770607: location_name.cloudy_park_2_s1, + 0x770608: location_name.cloudy_park_2_s2, + 0x770609: location_name.cloudy_park_2_s3, + 0x77060a: location_name.cloudy_park_2_s4, + 0x77060b: location_name.cloudy_park_2_s5, + 0x77060c: location_name.cloudy_park_2_s6, + 0x77060d: location_name.cloudy_park_2_s7, + 0x77060e: location_name.cloudy_park_2_s8, + 0x77060f: location_name.cloudy_park_2_s9, + 0x770610: location_name.cloudy_park_2_s10, + 0x770611: location_name.cloudy_park_2_s11, + 0x770612: location_name.cloudy_park_2_s12, + 0x770613: location_name.cloudy_park_2_s13, + 0x770614: location_name.cloudy_park_2_s14, + 0x770615: location_name.cloudy_park_2_s15, + 0x770616: location_name.cloudy_park_2_s16, + 0x770617: location_name.cloudy_park_2_s17, + 0x770618: location_name.cloudy_park_2_s18, + 0x770619: location_name.cloudy_park_2_s19, + 0x77061a: location_name.cloudy_park_2_s20, + 0x77061b: location_name.cloudy_park_2_s21, + 0x77061c: location_name.cloudy_park_2_s22, + 0x77061d: location_name.cloudy_park_2_s23, + 0x77061e: location_name.cloudy_park_2_s24, + 0x77061f: location_name.cloudy_park_2_s25, + 0x770620: location_name.cloudy_park_2_s26, + 0x770621: location_name.cloudy_park_2_s27, + 0x770622: location_name.cloudy_park_2_s28, + 0x770623: location_name.cloudy_park_2_s29, + 0x770624: location_name.cloudy_park_2_s30, + 0x770625: location_name.cloudy_park_2_s31, + 0x770626: location_name.cloudy_park_2_s32, + 0x770627: location_name.cloudy_park_2_s33, + 0x770628: location_name.cloudy_park_2_s34, + 0x770629: location_name.cloudy_park_2_s35, + 0x77062a: location_name.cloudy_park_2_s36, + 0x77062b: location_name.cloudy_park_2_s37, + 0x77062c: location_name.cloudy_park_2_s38, + 0x77062d: location_name.cloudy_park_2_s39, + 0x77062e: location_name.cloudy_park_2_s40, + 0x77062f: location_name.cloudy_park_2_s41, + 0x770630: location_name.cloudy_park_2_s42, + 0x770631: location_name.cloudy_park_2_s43, + 0x770632: location_name.cloudy_park_2_s44, + 0x770633: location_name.cloudy_park_2_s45, + 0x770634: location_name.cloudy_park_2_s46, + 0x770635: location_name.cloudy_park_2_s47, + 0x770636: location_name.cloudy_park_2_s48, + 0x770637: location_name.cloudy_park_2_s49, + 0x770638: location_name.cloudy_park_2_s50, + 0x770639: location_name.cloudy_park_2_s51, + 0x77063a: location_name.cloudy_park_2_s52, + 0x77063b: location_name.cloudy_park_2_s53, + 0x77063c: location_name.cloudy_park_2_s54, + 0x77063d: location_name.cloudy_park_3_s1, + 0x77063e: location_name.cloudy_park_3_s2, + 0x77063f: location_name.cloudy_park_3_s3, + 0x770640: location_name.cloudy_park_3_s4, + 0x770641: location_name.cloudy_park_3_s5, + 0x770642: location_name.cloudy_park_3_s6, + 0x770643: location_name.cloudy_park_3_s7, + 0x770644: location_name.cloudy_park_3_s8, + 0x770645: location_name.cloudy_park_3_s9, + 0x770646: location_name.cloudy_park_3_s10, + 0x770647: location_name.cloudy_park_3_s11, + 0x770648: location_name.cloudy_park_3_s12, + 0x770649: location_name.cloudy_park_3_s13, + 0x77064a: location_name.cloudy_park_3_s14, + 0x77064b: location_name.cloudy_park_3_s15, + 0x77064c: location_name.cloudy_park_3_s16, + 0x77064d: location_name.cloudy_park_3_s17, + 0x77064e: location_name.cloudy_park_3_s18, + 0x77064f: location_name.cloudy_park_3_s19, + 0x770650: location_name.cloudy_park_3_s20, + 0x770651: location_name.cloudy_park_3_s21, + 0x770652: location_name.cloudy_park_3_s22, + 0x770653: location_name.cloudy_park_4_s1, + 0x770654: location_name.cloudy_park_4_s2, + 0x770655: location_name.cloudy_park_4_s3, + 0x770656: location_name.cloudy_park_4_s4, + 0x770657: location_name.cloudy_park_4_s5, + 0x770658: location_name.cloudy_park_4_s6, + 0x770659: location_name.cloudy_park_4_s7, + 0x77065a: location_name.cloudy_park_4_s8, + 0x77065b: location_name.cloudy_park_4_s9, + 0x77065c: location_name.cloudy_park_4_s10, + 0x77065d: location_name.cloudy_park_4_s11, + 0x77065e: location_name.cloudy_park_4_s12, + 0x77065f: location_name.cloudy_park_4_s13, + 0x770660: location_name.cloudy_park_4_s14, + 0x770661: location_name.cloudy_park_4_s15, + 0x770662: location_name.cloudy_park_4_s16, + 0x770663: location_name.cloudy_park_4_s17, + 0x770664: location_name.cloudy_park_4_s18, + 0x770665: location_name.cloudy_park_4_s19, + 0x770666: location_name.cloudy_park_4_s20, + 0x770667: location_name.cloudy_park_4_s21, + 0x770668: location_name.cloudy_park_4_s22, + 0x770669: location_name.cloudy_park_4_s23, + 0x77066a: location_name.cloudy_park_4_s24, + 0x77066b: location_name.cloudy_park_4_s25, + 0x77066c: location_name.cloudy_park_4_s26, + 0x77066d: location_name.cloudy_park_4_s27, + 0x77066e: location_name.cloudy_park_4_s28, + 0x77066f: location_name.cloudy_park_4_s29, + 0x770670: location_name.cloudy_park_4_s30, + 0x770671: location_name.cloudy_park_4_s31, + 0x770672: location_name.cloudy_park_4_s32, + 0x770673: location_name.cloudy_park_4_s33, + 0x770674: location_name.cloudy_park_4_s34, + 0x770675: location_name.cloudy_park_4_s35, + 0x770676: location_name.cloudy_park_4_s36, + 0x770677: location_name.cloudy_park_4_s37, + 0x770678: location_name.cloudy_park_4_s38, + 0x770679: location_name.cloudy_park_4_s39, + 0x77067a: location_name.cloudy_park_4_s40, + 0x77067b: location_name.cloudy_park_4_s41, + 0x77067c: location_name.cloudy_park_4_s42, + 0x77067d: location_name.cloudy_park_4_s43, + 0x77067e: location_name.cloudy_park_4_s44, + 0x77067f: location_name.cloudy_park_4_s45, + 0x770680: location_name.cloudy_park_4_s46, + 0x770681: location_name.cloudy_park_4_s47, + 0x770682: location_name.cloudy_park_4_s48, + 0x770683: location_name.cloudy_park_4_s49, + 0x770684: location_name.cloudy_park_4_s50, + 0x770685: location_name.cloudy_park_5_s1, + 0x770686: location_name.cloudy_park_5_s2, + 0x770687: location_name.cloudy_park_5_s3, + 0x770688: location_name.cloudy_park_5_s4, + 0x770689: location_name.cloudy_park_5_s5, + 0x77068a: location_name.cloudy_park_5_s6, + 0x77068b: location_name.cloudy_park_6_s1, + 0x77068c: location_name.cloudy_park_6_s2, + 0x77068d: location_name.cloudy_park_6_s3, + 0x77068e: location_name.cloudy_park_6_s4, + 0x77068f: location_name.cloudy_park_6_s5, + 0x770690: location_name.cloudy_park_6_s6, + 0x770691: location_name.cloudy_park_6_s7, + 0x770692: location_name.cloudy_park_6_s8, + 0x770693: location_name.cloudy_park_6_s9, + 0x770694: location_name.cloudy_park_6_s10, + 0x770695: location_name.cloudy_park_6_s11, + 0x770696: location_name.cloudy_park_6_s12, + 0x770697: location_name.cloudy_park_6_s13, + 0x770698: location_name.cloudy_park_6_s14, + 0x770699: location_name.cloudy_park_6_s15, + 0x77069a: location_name.cloudy_park_6_s16, + 0x77069b: location_name.cloudy_park_6_s17, + 0x77069c: location_name.cloudy_park_6_s18, + 0x77069d: location_name.cloudy_park_6_s19, + 0x77069e: location_name.cloudy_park_6_s20, + 0x77069f: location_name.cloudy_park_6_s21, + 0x7706a0: location_name.cloudy_park_6_s22, + 0x7706a1: location_name.cloudy_park_6_s23, + 0x7706a2: location_name.cloudy_park_6_s24, + 0x7706a3: location_name.cloudy_park_6_s25, + 0x7706a4: location_name.cloudy_park_6_s26, + 0x7706a5: location_name.cloudy_park_6_s27, + 0x7706a6: location_name.cloudy_park_6_s28, + 0x7706a7: location_name.cloudy_park_6_s29, + 0x7706a8: location_name.cloudy_park_6_s30, + 0x7706a9: location_name.cloudy_park_6_s31, + 0x7706aa: location_name.cloudy_park_6_s32, + 0x7706ab: location_name.cloudy_park_6_s33, + 0x7706ac: location_name.iceberg_1_s1, + 0x7706ad: location_name.iceberg_1_s2, + 0x7706ae: location_name.iceberg_1_s3, + 0x7706af: location_name.iceberg_1_s4, + 0x7706b0: location_name.iceberg_1_s5, + 0x7706b1: location_name.iceberg_1_s6, + 0x7706b2: location_name.iceberg_2_s1, + 0x7706b3: location_name.iceberg_2_s2, + 0x7706b4: location_name.iceberg_2_s3, + 0x7706b5: location_name.iceberg_2_s4, + 0x7706b6: location_name.iceberg_2_s5, + 0x7706b7: location_name.iceberg_2_s6, + 0x7706b8: location_name.iceberg_2_s7, + 0x7706b9: location_name.iceberg_2_s8, + 0x7706ba: location_name.iceberg_2_s9, + 0x7706bb: location_name.iceberg_2_s10, + 0x7706bc: location_name.iceberg_2_s11, + 0x7706bd: location_name.iceberg_2_s12, + 0x7706be: location_name.iceberg_2_s13, + 0x7706bf: location_name.iceberg_2_s14, + 0x7706c0: location_name.iceberg_2_s15, + 0x7706c1: location_name.iceberg_2_s16, + 0x7706c2: location_name.iceberg_2_s17, + 0x7706c3: location_name.iceberg_2_s18, + 0x7706c4: location_name.iceberg_2_s19, + 0x7706c5: location_name.iceberg_3_s1, + 0x7706c6: location_name.iceberg_3_s2, + 0x7706c7: location_name.iceberg_3_s3, + 0x7706c8: location_name.iceberg_3_s4, + 0x7706c9: location_name.iceberg_3_s5, + 0x7706ca: location_name.iceberg_3_s6, + 0x7706cb: location_name.iceberg_3_s7, + 0x7706cc: location_name.iceberg_3_s8, + 0x7706cd: location_name.iceberg_3_s9, + 0x7706ce: location_name.iceberg_3_s10, + 0x7706cf: location_name.iceberg_3_s11, + 0x7706d0: location_name.iceberg_3_s12, + 0x7706d1: location_name.iceberg_3_s13, + 0x7706d2: location_name.iceberg_3_s14, + 0x7706d3: location_name.iceberg_3_s15, + 0x7706d4: location_name.iceberg_3_s16, + 0x7706d5: location_name.iceberg_3_s17, + 0x7706d6: location_name.iceberg_3_s18, + 0x7706d7: location_name.iceberg_3_s19, + 0x7706d8: location_name.iceberg_3_s20, + 0x7706d9: location_name.iceberg_3_s21, + 0x7706da: location_name.iceberg_4_s1, + 0x7706db: location_name.iceberg_4_s2, + 0x7706dc: location_name.iceberg_4_s3, + 0x7706dd: location_name.iceberg_5_s1, + 0x7706de: location_name.iceberg_5_s2, + 0x7706df: location_name.iceberg_5_s3, + 0x7706e0: location_name.iceberg_5_s4, + 0x7706e1: location_name.iceberg_5_s5, + 0x7706e2: location_name.iceberg_5_s6, + 0x7706e3: location_name.iceberg_5_s7, + 0x7706e4: location_name.iceberg_5_s8, + 0x7706e5: location_name.iceberg_5_s9, + 0x7706e6: location_name.iceberg_5_s10, + 0x7706e7: location_name.iceberg_5_s11, + 0x7706e8: location_name.iceberg_5_s12, + 0x7706e9: location_name.iceberg_5_s13, + 0x7706ea: location_name.iceberg_5_s14, + 0x7706eb: location_name.iceberg_5_s15, + 0x7706ec: location_name.iceberg_5_s16, + 0x7706ed: location_name.iceberg_5_s17, + 0x7706ee: location_name.iceberg_5_s18, + 0x7706ef: location_name.iceberg_5_s19, + 0x7706f0: location_name.iceberg_5_s20, + 0x7706f1: location_name.iceberg_5_s21, + 0x7706f2: location_name.iceberg_5_s22, + 0x7706f3: location_name.iceberg_5_s23, + 0x7706f4: location_name.iceberg_5_s24, + 0x7706f5: location_name.iceberg_5_s25, + 0x7706f6: location_name.iceberg_5_s26, + 0x7706f7: location_name.iceberg_5_s27, + 0x7706f8: location_name.iceberg_5_s28, + 0x7706f9: location_name.iceberg_5_s29, + 0x7706fa: location_name.iceberg_5_s30, + 0x7706fb: location_name.iceberg_5_s31, + 0x7706fc: location_name.iceberg_5_s32, + 0x7706fd: location_name.iceberg_5_s33, + 0x7706fe: location_name.iceberg_5_s34, + 0x7706ff: location_name.iceberg_6_s1, + +} + +location_table = { + **stage_locations, + **heart_star_locations, + **boss_locations, + **consumable_locations, + **star_locations +} diff --git a/worlds/kdl3/Names/__init__.py b/worlds/kdl3/names/__init__.py similarity index 100% rename from worlds/kdl3/Names/__init__.py rename to worlds/kdl3/names/__init__.py diff --git a/worlds/kdl3/Names/AnimalFriendSpawns.py b/worlds/kdl3/names/animal_friend_spawns.py similarity index 95% rename from worlds/kdl3/Names/AnimalFriendSpawns.py rename to worlds/kdl3/names/animal_friend_spawns.py index 4520cf143803..5c1ba3969748 100644 --- a/worlds/kdl3/Names/AnimalFriendSpawns.py +++ b/worlds/kdl3/names/animal_friend_spawns.py @@ -1,3 +1,5 @@ +from typing import List + grass_land_1_a1 = "Grass Land 1 - Animal 1" # Nago grass_land_1_a2 = "Grass Land 1 - Animal 2" # Rick grass_land_2_a1 = "Grass Land 2 - Animal 1" # ChuChu @@ -197,3 +199,12 @@ iceberg_6_a5: "ChuChu Spawn", iceberg_6_a6: "Nago Spawn", } + +problematic_sets: List[List[str]] = [ + # Animal groups that must be guaranteed unique. Potential for softlocks on future-ER if not. + [ripple_field_4_a1, ripple_field_4_a2, ripple_field_4_a3], + [sand_canyon_3_a1, sand_canyon_3_a2, sand_canyon_3_a3], + [cloudy_park_6_a1, cloudy_park_6_a2, cloudy_park_6_a3], + [iceberg_6_a1, iceberg_6_a2, iceberg_6_a3], + [iceberg_6_a4, iceberg_6_a5, iceberg_6_a6] +] diff --git a/worlds/kdl3/Names/EnemyAbilities.py b/worlds/kdl3/names/enemy_abilities.py similarity index 99% rename from worlds/kdl3/Names/EnemyAbilities.py rename to worlds/kdl3/names/enemy_abilities.py index 016e3033ab25..ace15054da59 100644 --- a/worlds/kdl3/Names/EnemyAbilities.py +++ b/worlds/kdl3/names/enemy_abilities.py @@ -809,7 +809,7 @@ enemy_restrictive: List[Tuple[List[str], List[str]]] = [ # abilities, enemies, set_all (False to set any) - (["Burning Ability", "Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7 + (["Stone Ability"], ["Rocky", "Sparky", "Babut", "Squishy", ]), # Ribbon Field 5 - 7 # Sand Canyon 6 (["Parasol Ability", "Cutter Ability"], ['Bukiset (Parasol)', 'Bukiset (Cutter)']), (["Spark Ability", "Clean Ability"], ['Bukiset (Spark)', 'Bukiset (Clean)']), diff --git a/worlds/kdl3/Names/LocationName.py b/worlds/kdl3/names/location_name.py similarity index 100% rename from worlds/kdl3/Names/LocationName.py rename to worlds/kdl3/names/location_name.py diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/options.py similarity index 82% rename from worlds/kdl3/Options.py rename to worlds/kdl3/options.py index e0a4f12f15dc..b9163794ad19 100644 --- a/worlds/kdl3/Options.py +++ b/worlds/kdl3/options.py @@ -1,13 +1,21 @@ import random from dataclasses import dataclass +from typing import List -from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ - PerGameCommonOptions, PlandoConnections -from .Names import LocationName +from Options import DeathLinkMixin, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ + PerGameCommonOptions, Visibility, NamedRange, OptionGroup, PlandoConnections +from .names import location_name + + +class RemoteItems(DefaultOnToggle): + """ + Enables receiving items from your own world, primarily for co-op play. + """ + display_name = "Remote Items" class KDL3PlandoConnections(PlandoConnections): - entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)} + entrances = exits = {f"{i} {j}" for i in location_name.level_names for j in range(1, 7)} class Goal(Choice): @@ -30,6 +38,7 @@ def get_option_name(cls, value: int) -> str: return cls.name_lookup[value].upper() return super().get_option_name(value) + class GoalSpeed(Choice): """ Normal: the goal is unlocked after purifying the five bosses @@ -40,13 +49,14 @@ class GoalSpeed(Choice): option_fast = 1 -class TotalHeartStars(Range): +class MaxHeartStars(Range): """ Maximum number of heart stars to include in the pool of items. + If fewer available locations exist in the pool than this number, the number of available locations will be used instead. """ display_name = "Max Heart Stars" range_start = 5 # set to 5 so strict bosses does not degrade - range_end = 50 # 30 default locations + 30 stage clears + 5 bosses - 14 progression items = 51, so round down + range_end = 99 # previously set to 50, set to highest it can be should there be less locations than heart stars default = 30 @@ -84,9 +94,9 @@ class BossShuffle(PlandoBosses): Singularity: All (non-Zero) bosses will be replaced with a single boss Supports plando placement. """ - bosses = frozenset(LocationName.boss_names.keys()) + bosses = frozenset(location_name.boss_names.keys()) - locations = frozenset(LocationName.level_names.keys()) + locations = frozenset(location_name.level_names.keys()) duplicate_bosses = True @@ -278,7 +288,8 @@ class KirbyFlavorPreset(Choice): option_orange = 11 option_lime = 12 option_lavender = 13 - option_custom = 14 + option_miku = 14 + option_custom = 15 default = 0 @classmethod @@ -296,6 +307,7 @@ class KirbyFlavor(OptionDict): A custom color for Kirby. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to "15", with their values being an HTML hex color. """ + display_name = "Custom Kirby Flavor" default = { "1": "B01810", "2": "F0E0E8", @@ -313,6 +325,7 @@ class KirbyFlavor(OptionDict): "14": "F8F8F8", "15": "B03830", } + visibility = Visibility.template | Visibility.spoiler # likely never supported on guis class GooeyFlavorPreset(Choice): @@ -352,6 +365,7 @@ class GooeyFlavor(OptionDict): A custom color for Gooey. To use a custom color, set the preset to Custom and then define a dict of keys from "1" to "15", with their values being an HTML hex color. """ + display_name = "Custom Gooey Flavor" default = { "1": "000808", "2": "102838", @@ -363,6 +377,7 @@ class GooeyFlavor(OptionDict): "8": "D0C0C0", "9": "F8F8F8", } + visibility = Visibility.template | Visibility.spoiler # likely never supported on guis class MusicShuffle(Choice): @@ -402,14 +417,27 @@ class Gifting(Toggle): display_name = "Gifting" +class TotalHeartStars(NamedRange): + """ + Deprecated. Use max_heart_stars instead. Supported for only one version. + """ + default = -1 + range_start = 5 + range_end = 99 + special_range_names = { + "default": -1 + } + visibility = Visibility.none + + @dataclass -class KDL3Options(PerGameCommonOptions): +class KDL3Options(PerGameCommonOptions, DeathLinkMixin): + remote_items: RemoteItems plando_connections: KDL3PlandoConnections - death_link: DeathLink game_language: GameLanguage goal: Goal goal_speed: GoalSpeed - total_heart_stars: TotalHeartStars + max_heart_stars: MaxHeartStars heart_stars_required: HeartStarsRequired filler_percentage: FillerPercentage trap_percentage: TrapPercentage @@ -435,3 +463,17 @@ class KDL3Options(PerGameCommonOptions): gooey_flavor: GooeyFlavor music_shuffle: MusicShuffle virtual_console: VirtualConsoleChanges + + total_heart_stars: TotalHeartStars # remove in 2 versions + + +kdl3_option_groups: List[OptionGroup] = [ + OptionGroup("Goal Options", [Goal, GoalSpeed, MaxHeartStars, HeartStarsRequired, JumpingTarget, ]), + OptionGroup("World Options", [RemoteItems, StrictBosses, OpenWorld, OpenWorldBossRequirement, ConsumableChecks, + StarChecks, FillerPercentage, TrapPercentage, GooeyTrapPercentage, + SlowTrapPercentage, AbilityTrapPercentage, LevelShuffle, BossShuffle, + AnimalRandomization, CopyAbilityRandomization, BossRequirementRandom, + Gifting, ]), + OptionGroup("Cosmetic Options", [GameLanguage, BossShuffleAllowBB, KirbyFlavorPreset, KirbyFlavor, + GooeyFlavorPreset, GooeyFlavor, MusicShuffle, VirtualConsoleChanges, ]), +] diff --git a/worlds/kdl3/Presets.py b/worlds/kdl3/presets.py similarity index 98% rename from worlds/kdl3/Presets.py rename to worlds/kdl3/presets.py index d3a7146ded5f..491ad9dca993 100644 --- a/worlds/kdl3/Presets.py +++ b/worlds/kdl3/presets.py @@ -25,6 +25,7 @@ "ow_boss_requirement": "random", "boss_requirement_random": "random", "consumables": "random", + "starsanity": "random", "kirby_flavor_preset": "random", "gooey_flavor_preset": "random", "music_shuffle": "random", diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/regions.py similarity index 66% rename from worlds/kdl3/Regions.py rename to worlds/kdl3/regions.py index 407dcf9680f4..c47e5dee4095 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/regions.py @@ -1,60 +1,62 @@ import orjson import os from pkgutil import get_data +from copy import deepcopy -from typing import TYPE_CHECKING, List, Dict, Optional, Union -from BaseClasses import Region +from typing import TYPE_CHECKING, List, Dict, Optional, Union, Callable +from BaseClasses import Region, CollectionState from worlds.generic.Rules import add_item_rule -from .Locations import KDL3Location -from .Names import LocationName -from .Options import BossShuffle -from .Room import KDL3Room +from .locations import KDL3Location +from .names import location_name +from .options import BossShuffle +from .room import KDL3Room if TYPE_CHECKING: from . import KDL3World default_levels = { - 1: [0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770006, 0x770200], - 2: [0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x77000C, 0x770201], - 3: [0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770012, 0x770202], - 4: [0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770018, 0x770203], - 5: [0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x77001E, 0x770204], + 1: [0x770000, 0x770001, 0x770002, 0x770003, 0x770004, 0x770005, 0x770200], + 2: [0x770006, 0x770007, 0x770008, 0x770009, 0x77000A, 0x77000B, 0x770201], + 3: [0x77000C, 0x77000D, 0x77000E, 0x77000F, 0x770010, 0x770011, 0x770202], + 4: [0x770012, 0x770013, 0x770014, 0x770015, 0x770016, 0x770017, 0x770203], + 5: [0x770018, 0x770019, 0x77001A, 0x77001B, 0x77001C, 0x77001D, 0x770204], } first_stage_blacklist = { # We want to confirm that the first stage can be completed without any items - 0x77000B, # 2-5 needs Kine - 0x770011, # 3-5 needs Cutter - 0x77001C, # 5-4 needs Burning + 0x77000A, # 2-5 needs Kine + 0x770010, # 3-5 needs Cutter + 0x77001B, # 5-4 needs Burning } first_world_limit = { # We need to limit the number of very restrictive stages in level 1 on solo gens *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks + 0x770006, 0x770007, - 0x770008, - 0x770013, - 0x77001E, + 0x770012, + 0x77001D, } def generate_valid_level(world: "KDL3World", level: int, stage: int, - possible_stages: List[int], placed_stages: List[int]): + possible_stages: List[int], placed_stages: List[Optional[int]]) -> int: new_stage = world.random.choice(possible_stages) if level == 1: if stage == 0 and new_stage in first_stage_blacklist: + possible_stages.remove(new_stage) return generate_valid_level(world, level, stage, possible_stages, placed_stages) elif (not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and - new_stage in first_world_limit and - sum(p_stage in first_world_limit for p_stage in placed_stages) + new_stage in first_world_limit and + sum(p_stage in first_world_limit for p_stage in placed_stages) >= (2 if world.options.open_world else 1)): return generate_valid_level(world, level, stage, possible_stages, placed_stages) return new_stage -def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): - level_names = {LocationName.level_names[level]: level for level in LocationName.level_names} +def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]) -> None: + level_names = {location_name.level_names[level]: level for level in location_name.level_names} room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) rooms: Dict[str, KDL3Room] = dict() for room_entry in room_data: @@ -63,7 +65,7 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): room_entry["default_exits"], room_entry["animal_pointers"], room_entry["enemies"], room_entry["entity_load"], room_entry["consumables"], room_entry["consumables_pointer"]) room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else - None for location in room_entry["locations"] + None for location in room_entry["locations"] if (not any(x in location for x in ["1-Up", "Maxim"]) or world.options.consumables.value) and ("Star" not in location or world.options.starsanity.value)}, @@ -83,8 +85,8 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): if room.stage == 7: first_rooms[0x770200 + room.level - 1] = room else: - first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room - exits = dict() + first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage - 1] = room + exits: Dict[str, Callable[[CollectionState], bool]] = dict() for def_exit in room.default_exits: target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" access_rule = tuple(def_exit["access_rule"]) @@ -115,50 +117,54 @@ def generate_rooms(world: "KDL3World", level_regions: Dict[int, Region]): if world.options.open_world: level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) else: - world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\ + world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player) \ .parent_region.add_exits([first_rooms[0x770200 + level - 1].name]) -def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict: - levels: Dict[int, List[Optional[int]]] = { - 1: [None] * 7, - 2: [None] * 7, - 3: [None] * 7, - 4: [None] * 7, - 5: [None] * 7, - } +def generate_valid_levels(world: "KDL3World", shuffle_mode: int) -> Dict[int, List[int]]: + if shuffle_mode: + levels: Dict[int, List[Optional[int]]] = { + 1: [None] * 7, + 2: [None] * 7, + 3: [None] * 7, + 4: [None] * 7, + 5: [None] * 7, + } + + possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] + if world.options.plando_connections: + for connection in world.options.plando_connections: + try: + entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) + stage_world, stage_stage = connection.exit.rsplit(" ", 1) + new_stage = default_levels[location_name.level_names[stage_world.strip()]][int(stage_stage) - 1] + levels[location_name.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage + possible_stages.remove(new_stage) - possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] - if world.options.plando_connections: - for connection in world.options.plando_connections: - try: - entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) - stage_world, stage_stage = connection.exit.rsplit(" ", 1) - new_stage = default_levels[LocationName.level_names[stage_world.strip()]][int(stage_stage) - 1] - levels[LocationName.level_names[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage - possible_stages.remove(new_stage) - - except Exception: - raise Exception( - f"Invalid connection: {connection.entrance} =>" - f" {connection.exit} for player {world.player} ({world.player_name})") - - for level in range(1, 6): - for stage in range(6): - # Randomize bosses separately - try: + except Exception: + raise Exception( + f"Invalid connection: {connection.entrance} =>" + f" {connection.exit} for player {world.player} ({world.player_name})") + + for level in range(1, 6): + for stage in range(6): + # Randomize bosses separately if levels[level][stage] is None: stage_candidates = [candidate for candidate in possible_stages - if (enforce_world and candidate in default_levels[level]) - or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage) - or (enforce_pattern == enforce_world) + if (shuffle_mode == 1 and candidate in default_levels[level]) + or (shuffle_mode == 2 and (candidate & 0x00FFFF) % 6 == stage) + or (shuffle_mode == 3) ] + if not stage_candidates: + raise Exception( + f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}") new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level]) possible_stages.remove(new_stage) levels[level][stage] = new_stage - except Exception: - raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}") - + else: + levels = deepcopy(default_levels) + for level in levels: + levels[level][6] = None # now handle bosses boss_shuffle: Union[int, str] = world.options.boss_shuffle.value plando_bosses = [] @@ -168,17 +174,17 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte boss_shuffle = BossShuffle.options[options.pop()] for option in options: if "-" in option: - loc, boss = option.split("-") + loc, plando_boss = option.split("-") loc = loc.title() - boss = boss.title() - levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss] - plando_bosses.append(LocationName.boss_names[boss]) + plando_boss = plando_boss.title() + levels[location_name.level_names[loc]][6] = location_name.boss_names[plando_boss] + plando_bosses.append(location_name.boss_names[plando_boss]) else: option = option.title() for level in levels: if levels[level][6] is None: - levels[level][6] = LocationName.boss_names[option] - plando_bosses.append(LocationName.boss_names[option]) + levels[level][6] = location_name.boss_names[option] + plando_bosses.append(location_name.boss_names[option]) if boss_shuffle > 0: if boss_shuffle == BossShuffle.option_full: @@ -223,15 +229,14 @@ def create_levels(world: "KDL3World") -> None: 5: level5, } level_shuffle = world.options.stage_shuffle.value - if level_shuffle != 0: - world.player_levels = generate_valid_levels( - world, - level_shuffle == 1, - level_shuffle == 2) + if hasattr(world.multiworld, "re_gen_passthrough"): + world.player_levels = getattr(world.multiworld, "re_gen_passthrough")["Kirby's Dream Land 3"]["player_levels"] + else: + world.player_levels = generate_valid_levels(world, level_shuffle) generate_rooms(world, levels) - level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location) + level6.add_locations({location_name.goals[world.options.goal.value]: None}, KDL3Location) menu.connect(level1, "Start Game") level1.connect(level2, "To Level 2") diff --git a/worlds/kdl3/rom.py b/worlds/kdl3/rom.py new file mode 100644 index 000000000000..3dd10ce1c43f --- /dev/null +++ b/worlds/kdl3/rom.py @@ -0,0 +1,602 @@ +import typing +from pkgutil import get_data + +import Utils +from typing import Optional, TYPE_CHECKING, Tuple, Dict, List +import hashlib +import os +import struct + +import settings +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension +from .aesthetics import get_palette_bytes, kirby_target_palettes, get_kirby_palette, gooey_target_palettes, \ + get_gooey_palette +from .compression import hal_decompress +import bsdiff4 + +if TYPE_CHECKING: + from . import KDL3World + +KDL3UHASH = "201e7658f6194458a3869dde36bf8ec2" +KDL3JHASH = "b2f2d004ea640c3db66df958fce122b2" + +level_pointers = { + 0x770000: 0x0084, + 0x770001: 0x009C, + 0x770002: 0x00B8, + 0x770003: 0x00D8, + 0x770004: 0x0104, + 0x770005: 0x0124, + 0x770006: 0x014C, + 0x770007: 0x0170, + 0x770008: 0x0190, + 0x770009: 0x01B0, + 0x77000A: 0x01E8, + 0x77000B: 0x0218, + 0x77000C: 0x024C, + 0x77000D: 0x0270, + 0x77000E: 0x02A0, + 0x77000F: 0x02C4, + 0x770010: 0x02EC, + 0x770011: 0x0314, + 0x770012: 0x03CC, + 0x770013: 0x0404, + 0x770014: 0x042C, + 0x770015: 0x044C, + 0x770016: 0x0478, + 0x770017: 0x049C, + 0x770018: 0x04E4, + 0x770019: 0x0504, + 0x77001A: 0x0530, + 0x77001B: 0x0554, + 0x77001C: 0x05A8, + 0x77001D: 0x0640, + 0x770200: 0x0148, + 0x770201: 0x0248, + 0x770202: 0x03C8, + 0x770203: 0x04E0, + 0x770204: 0x06A4, + 0x770205: 0x06A8, +} + +bb_bosses = { + 0x770200: 0xED85F1, + 0x770201: 0xF01360, + 0x770202: 0xEDA3DF, + 0x770203: 0xEDC2B9, + 0x770204: 0xED7C3F, + 0x770205: 0xEC29D2, +} + +level_sprites = { + 0x19B2C6: 1827, + 0x1A195C: 1584, + 0x19F6F3: 1679, + 0x19DC8B: 1717, + 0x197900: 1872 +} + +stage_tiles = { + 0: [ + 0, 1, 2, + 16, 17, 18, + 32, 33, 34, + 48, 49, 50 + ], + 1: [ + 3, 4, 5, + 19, 20, 21, + 35, 36, 37, + 51, 52, 53 + ], + 2: [ + 6, 7, 8, + 22, 23, 24, + 38, 39, 40, + 54, 55, 56 + ], + 3: [ + 9, 10, 11, + 25, 26, 27, + 41, 42, 43, + 57, 58, 59, + ], + 4: [ + 12, 13, 64, + 28, 29, 65, + 44, 45, 66, + 60, 61, 67 + ], + 5: [ + 14, 15, 68, + 30, 31, 69, + 46, 47, 70, + 62, 63, 71 + ] +} + +heart_star_address = 0x2D0000 +heart_star_size = 456 +consumable_address = 0x2F91DD +consumable_size = 698 + +stage_palettes = [0x60964, 0x60B64, 0x60D64, 0x60F64, 0x61164] + +music_choices = [ + 2, # Boss 1 + 3, # Boss 2 (Unused) + 4, # Boss 3 (Miniboss) + 7, # Dedede + 9, # Event 2 (used once) + 10, # Field 1 + 11, # Field 2 + 12, # Field 3 + 13, # Field 4 + 14, # Field 5 + 15, # Field 6 + 16, # Field 7 + 17, # Field 8 + 18, # Field 9 + 19, # Field 10 + 20, # Field 11 + 21, # Field 12 (Gourmet Race) + 23, # Dark Matter in the Hyper Zone + 24, # Zero + 25, # Level 1 + 26, # Level 2 + 27, # Level 4 + 28, # Level 3 + 29, # Heart Star Failed + 30, # Level 5 + 31, # Minigame + 38, # Animal Friend 1 + 39, # Animal Friend 2 + 40, # Animal Friend 3 +] +# extra room pointers we don't want to track other than for music +room_music = { + 3079990: 23, # Zero + 2983409: 2, # BB Whispy + 3150688: 2, # BB Acro + 2991071: 2, # BB PonCon + 2998969: 2, # BB Ado + 2980927: 7, # BB Dedede + 2894290: 23 # BB Zero +} + +enemy_remap = { + "Waddle Dee": 0, + "Bronto Burt": 2, + "Rocky": 3, + "Bobo": 5, + "Chilly": 6, + "Poppy Bros Jr.": 7, + "Sparky": 8, + "Polof": 9, + "Broom Hatter": 11, + "Cappy": 12, + "Bouncy": 13, + "Nruff": 15, + "Glunk": 16, + "Togezo": 18, + "Kabu": 19, + "Mony": 20, + "Blipper": 21, + "Squishy": 22, + "Gabon": 24, + "Oro": 25, + "Galbo": 26, + "Sir Kibble": 27, + "Nidoo": 28, + "Kany": 29, + "Sasuke": 30, + "Yaban": 32, + "Boten": 33, + "Coconut": 34, + "Doka": 35, + "Icicle": 36, + "Pteran": 39, + "Loud": 40, + "Como": 41, + "Klinko": 42, + "Babut": 43, + "Wappa": 44, + "Mariel": 45, + "Tick": 48, + "Apolo": 49, + "Popon Ball": 50, + "KeKe": 51, + "Magoo": 53, + "Raft Waddle Dee": 57, + "Madoo": 58, + "Corori": 60, + "Kapar": 67, + "Batamon": 68, + "Peran": 72, + "Bobin": 73, + "Mopoo": 74, + "Gansan": 75, + "Bukiset (Burning)": 76, + "Bukiset (Stone)": 77, + "Bukiset (Ice)": 78, + "Bukiset (Needle)": 79, + "Bukiset (Clean)": 80, + "Bukiset (Parasol)": 81, + "Bukiset (Spark)": 82, + "Bukiset (Cutter)": 83, + "Waddle Dee Drawing": 84, + "Bronto Burt Drawing": 85, + "Bouncy Drawing": 86, + "Kabu (Dekabu)": 87, + "Wapod": 88, + "Propeller": 89, + "Dogon": 90, + "Joe": 91 +} + +miniboss_remap = { + "Captain Stitch": 0, + "Yuki": 1, + "Blocky": 2, + "Jumper Shoot": 3, + "Boboo": 4, + "Haboki": 5 +} + +ability_remap = { + "No Ability": 0, + "Burning Ability": 1, + "Stone Ability": 2, + "Ice Ability": 3, + "Needle Ability": 4, + "Clean Ability": 5, + "Parasol Ability": 6, + "Spark Ability": 7, + "Cutter Ability": 8, +} + + +class RomData: + def __init__(self, file: bytes, name: typing.Optional[str] = None): + self.file = bytearray(file) + self.name = name + + def read_byte(self, offset: int) -> int: + return self.file[offset] + + def read_bytes(self, offset: int, length: int) -> bytearray: + return self.file[offset:offset + length] + + def write_byte(self, offset: int, value: int) -> None: + self.file[offset] = value + + def write_bytes(self, offset: int, values: typing.Sequence[int]) -> None: + self.file[offset:offset + len(values)] = values + + def get_bytes(self) -> bytes: + return bytes(self.file) + + +def handle_level_sprites(stages: List[Tuple[int, ...]], sprites: List[bytearray], palettes: List[List[bytearray]]) \ + -> Tuple[List[bytearray], List[bytearray]]: + palette_by_level = list() + for palette in palettes: + palette_by_level.extend(palette[10:16]) + out_palettes = list() + for i in range(5): + for j in range(6): + palettes[i][10 + j] = palette_by_level[stages[i][j]] + out_palettes.append(bytearray([x for palette in palettes[i] for x in palette])) + tiles_by_level = list() + for spritesheet in sprites: + decompressed = hal_decompress(spritesheet) + tiles = [decompressed[i:i + 32] for i in range(0, 2304, 32)] + tiles_by_level.extend([[tiles[x] for x in stage_tiles[stage]] for stage in stage_tiles]) + out_sprites = list() + for world in range(5): + levels = [stages[world][x] for x in range(6)] + world_tiles: typing.List[bytes] = [bytes() for _ in range(72)] + for i in range(6): + for x in range(12): + world_tiles[stage_tiles[i][x]] = tiles_by_level[levels[i]][x] + out_sprites.append(bytearray()) + for tile in world_tiles: + out_sprites[world].extend(tile) + # insert our fake compression + out_sprites[world][0:0] = [0xe3, 0xff] + out_sprites[world][1026:1026] = [0xe3, 0xff] + out_sprites[world][2052:2052] = [0xe0, 0xff] + out_sprites[world].append(0xff) + return out_sprites, out_palettes + + +def write_heart_star_sprites(rom: RomData) -> None: + compressed = rom.read_bytes(heart_star_address, heart_star_size) + decompressed = hal_decompress(compressed) + patch = get_data(__name__, os.path.join("data", "APHeartStar.bsdiff4")) + patched = bytearray(bsdiff4.patch(decompressed, patch)) + rom.write_bytes(0x1AF7DF, patched) + patched[0:0] = [0xE3, 0xFF] + patched.append(0xFF) + rom.write_bytes(0x1CD000, patched) + rom.write_bytes(0x3F0EBF, [0x00, 0xD0, 0x39]) + + +def write_consumable_sprites(rom: RomData, consumables: bool, stars: bool) -> None: + compressed = rom.read_bytes(consumable_address, consumable_size) + decompressed = hal_decompress(compressed) + patched = bytearray(decompressed) + if consumables: + patch = get_data(__name__, os.path.join("data", "APConsumable.bsdiff4")) + patched = bytearray(bsdiff4.patch(bytes(patched), patch)) + if stars: + patch = get_data(__name__, os.path.join("data", "APStars.bsdiff4")) + patched = bytearray(bsdiff4.patch(bytes(patched), patch)) + patched[0:0] = [0xE3, 0xFF] + patched.append(0xFF) + rom.write_bytes(0x1CD500, patched) + rom.write_bytes(0x3F0DAE, [0x00, 0xD5, 0x39]) + + +class KDL3PatchExtensions(APPatchExtension): + game = "Kirby's Dream Land 3" + + @staticmethod + def apply_post_patch(_: APProcedurePatch, rom: bytes) -> bytes: + rom_data = RomData(rom) + write_heart_star_sprites(rom_data) + if rom_data.read_bytes(0x3D014, 1)[0] > 0: + stages = [struct.unpack("HHHHHHH", rom_data.read_bytes(0x3D020 + x * 14, 14)) for x in range(5)] + palettes = [rom_data.read_bytes(full_pal, 512) for full_pal in stage_palettes] + read_palettes = [[palette[i:i + 32] for i in range(0, 512, 32)] for palette in palettes] + sprites = [rom_data.read_bytes(offset, level_sprites[offset]) for offset in level_sprites] + sprites, palettes = handle_level_sprites(stages, sprites, read_palettes) + for addr, palette in zip(stage_palettes, palettes): + rom_data.write_bytes(addr, palette) + for addr, level_sprite in zip([0x1CA000, 0x1CA920, 0x1CB230, 0x1CBB40, 0x1CC450], sprites): + rom_data.write_bytes(addr, level_sprite) + rom_data.write_bytes(0x460A, [0x00, 0xA0, 0x39, 0x20, 0xA9, 0x39, 0x30, 0xB2, 0x39, 0x40, 0xBB, 0x39, + 0x50, 0xC4, 0x39]) + write_consumable_sprites(rom_data, rom_data.read_byte(0x3D018) > 0, rom_data.read_byte(0x3D01A) > 0) + return rom_data.get_bytes() + + +class KDL3ProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [KDL3UHASH, KDL3JHASH] + game = "Kirby's Dream Land 3" + patch_file_ending = ".apkdl3" + procedure = [ + ("apply_bsdiff4", ["kdl3_basepatch.bsdiff4"]), + ("apply_tokens", ["token_patch.bin"]), + ("apply_post_patch", []), + ("calc_snes_crc", []) + ] + name: bytes # used to pass to __init__ + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def patch_rom(world: "KDL3World", patch: KDL3ProcedurePatch) -> None: + patch.write_file("kdl3_basepatch.bsdiff4", + get_data(__name__, os.path.join("data", "kdl3_basepatch.bsdiff4"))) + + # Write open world patch + if world.options.open_world: + patch.write_token(APTokenTypes.WRITE, 0x143C7, bytes([0xAD, 0xC1, 0x5A, 0xCD, 0xC1, 0x5A, ])) + # changes the stage flag function to compare $5AC1 to $5AC1, + # always running the "new stage" function + # This has further checks present for bosses already, so we just + # need to handle regular stages + # write check for boss to be unlocked + + if world.options.consumables: + # reroute maxim tomatoes to use the 1-UP function, then null out the function + patch.write_token(APTokenTypes.WRITE, 0x3002F, bytes([0x37, 0x00])) + patch.write_token(APTokenTypes.WRITE, 0x30037, bytes([0xA9, 0x26, 0x00, # LDA #$0026 + 0x22, 0x27, 0xD9, 0x00, # JSL $00D927 + 0xA4, 0xD2, # LDY $D2 + 0x6B, # RTL + 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA, + 0xEA, # NOP #10 + ])) + + # stars handling is built into the rom, so no changes there + + rooms = world.rooms + if world.options.music_shuffle > 0: + if world.options.music_shuffle == 1: + shuffled_music = music_choices.copy() + world.random.shuffle(shuffled_music) + music_map = dict(zip(music_choices, shuffled_music)) + # Avoid putting star twinkle in the pool + music_map[5] = world.random.choice(music_choices) + # Heart Star music doesn't work on regular stages + music_map[8] = world.random.choice(music_choices) + for room in rooms: + room.music = music_map[room.music] + for room_ptr in room_music: + patch.write_token(APTokenTypes.WRITE, room_ptr + 2, bytes([music_map[room_music[room_ptr]]])) + for i, old_music in zip(range(5), [25, 26, 28, 27, 30]): + # level themes + patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, bytes([music_map[old_music]])) + # Zero + patch.write_token(APTokenTypes.WRITE, 0x9AE79, music_map[0x18].to_bytes(1, "little")) + # Heart Star success and fail + patch.write_token(APTokenTypes.WRITE, 0x4A388, music_map[0x08].to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x4A38D, music_map[0x1D].to_bytes(1, "little")) + elif world.options.music_shuffle == 2: + for room in rooms: + room.music = world.random.choice(music_choices) + for room_ptr in room_music: + patch.write_token(APTokenTypes.WRITE, room_ptr + 2, + world.random.choice(music_choices).to_bytes(1, "little")) + for i in range(5): + # level themes + patch.write_token(APTokenTypes.WRITE, 0x133F2 + i, + world.random.choice(music_choices).to_bytes(1, "little")) + # Zero + patch.write_token(APTokenTypes.WRITE, 0x9AE79, world.random.choice(music_choices).to_bytes(1, "little")) + # Heart Star success and fail + patch.write_token(APTokenTypes.WRITE, 0x4A388, world.random.choice(music_choices).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x4A38D, world.random.choice(music_choices).to_bytes(1, "little")) + + for room in rooms: + room.patch(patch, bool(world.options.consumables.value), not bool(world.options.remote_items.value)) + + if world.options.virtual_console in [1, 3]: + # Flash Reduction + patch.write_token(APTokenTypes.WRITE, 0x9AE68, b"\x10") + patch.write_token(APTokenTypes.WRITE, 0x9AE8E, bytes([0x08, 0x00, 0x22, 0x5D, 0xF7, 0x00, 0xA2, 0x08, ])) + patch.write_token(APTokenTypes.WRITE, 0x9AEA1, b"\x08") + patch.write_token(APTokenTypes.WRITE, 0x9AEC9, b"\x01") + patch.write_token(APTokenTypes.WRITE, 0x9AED2, bytes([0xA9, 0x1F])) + patch.write_token(APTokenTypes.WRITE, 0x9AEE1, b"\x08") + + if world.options.virtual_console in [2, 3]: + # Hyper Zone BB colors + patch.write_token(APTokenTypes.WRITE, 0x2C5E16, bytes([0xEE, 0x1B, 0x18, 0x5B, 0xD3, 0x4A, 0xF4, 0x3B, ])) + patch.write_token(APTokenTypes.WRITE, 0x2C8217, bytes([0xFF, 0x1E, ])) + + # boss requirements + patch.write_token(APTokenTypes.WRITE, 0x3D000, + struct.pack("HHHHH", world.boss_requirements[0], world.boss_requirements[1], + world.boss_requirements[2], world.boss_requirements[3], + world.boss_requirements[4])) + patch.write_token(APTokenTypes.WRITE, 0x3D00A, + struct.pack("H", world.required_heart_stars if world.options.goal_speed == 1 else 0xFFFF)) + patch.write_token(APTokenTypes.WRITE, 0x3D00C, world.options.goal_speed.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D00E, world.options.open_world.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D010, ((world.options.remote_items.value << 1) + + world.options.death_link.value).to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D012, world.options.goal.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D014, world.options.stage_shuffle.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D016, world.options.ow_boss_requirement.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D018, world.options.consumables.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D01A, world.options.starsanity.value.to_bytes(2, "little")) + patch.write_token(APTokenTypes.WRITE, 0x3D01C, world.options.gifting.value.to_bytes(2, "little") + if world.multiworld.players > 1 else bytes([0, 0])) + patch.write_token(APTokenTypes.WRITE, 0x3D01E, world.options.strict_bosses.value.to_bytes(2, "little")) + # don't write gifting for solo game, since there's no one to send anything to + + for level in world.player_levels: + for i in range(len(world.player_levels[level])): + patch.write_token(APTokenTypes.WRITE, 0x3F002E + ((level - 1) * 14) + (i * 2), + struct.pack("H", level_pointers[world.player_levels[level][i]])) + patch.write_token(APTokenTypes.WRITE, 0x3D020 + (level - 1) * 14 + (i * 2), + struct.pack("H", world.player_levels[level][i] & 0x00FFFF)) + if (i == 0) or (i > 0 and i % 6 != 0): + patch.write_token(APTokenTypes.WRITE, 0x3D080 + (level - 1) * 12 + (i * 2), + struct.pack("H", (world.player_levels[level][i] & 0x00FFFF) % 6)) + + for i in range(6): + if world.boss_butch_bosses[i]: + patch.write_token(APTokenTypes.WRITE, 0x3F0000 + (level_pointers[0x770200 + i]), + struct.pack("I", bb_bosses[0x770200 + i])) + + # copy ability shuffle + if world.options.copy_ability_randomization.value > 0: + for enemy in world.copy_abilities: + if enemy in miniboss_remap: + patch.write_token(APTokenTypes.WRITE, 0xB417E + (miniboss_remap[enemy] << 1), + struct.pack("H", ability_remap[world.copy_abilities[enemy]])) + else: + patch.write_token(APTokenTypes.WRITE, 0xB3CAC + (enemy_remap[enemy] << 1), + struct.pack("H", ability_remap[world.copy_abilities[enemy]])) + # following only needs done on non-door rando + # incredibly lucky this follows the same order (including 5E == star block) + patch.write_token(APTokenTypes.WRITE, 0x2F77EA, + (0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F7811, + (0x5E + (ability_remap[world.copy_abilities["Sparky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9BC4, + (0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9BEB, + (0x5E + (ability_remap[world.copy_abilities["Blocky"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FAC06, + (0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FAC2D, + (0x5E + (ability_remap[world.copy_abilities["Jumper Shoot"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9E7B, + (0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9EA2, + (0x5E + (ability_remap[world.copy_abilities["Yuki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA951, + (0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA978, + (0x5E + (ability_remap[world.copy_abilities["Sir Kibble"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA132, + (0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA159, + (0x5E + (ability_remap[world.copy_abilities["Haboki"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA3E8, + (0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2FA40F, + (0x5E + (ability_remap[world.copy_abilities["Boboo"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F90E2, + (0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little")) + patch.write_token(APTokenTypes.WRITE, 0x2F9109, + (0x5E + (ability_remap[world.copy_abilities["Captain Stitch"]] << 1)).to_bytes(1, "little")) + + if world.options.copy_ability_randomization == 2: + for enemy in enemy_remap: + # we just won't include it for minibosses + patch.write_token(APTokenTypes.WRITE, 0xB3E40 + (enemy_remap[enemy] << 1), + struct.pack("h", world.random.randint(-1, 2))) + + # write jumping goal + patch.write_token(APTokenTypes.WRITE, 0x94F8, struct.pack("H", world.options.jumping_target)) + patch.write_token(APTokenTypes.WRITE, 0x944E, struct.pack("H", world.options.jumping_target)) + + from Utils import __version__ + patch_name = bytearray( + f'KDL3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] + patch_name.extend([0] * (21 - len(patch_name))) + patch.name = bytes(patch_name) + patch.write_token(APTokenTypes.WRITE, 0x3C000, patch.name) + patch.write_token(APTokenTypes.WRITE, 0x3C020, world.options.game_language.value.to_bytes(1, "little")) + + patch.write_token(APTokenTypes.COPY, 0x7FC0, (21, 0x3C000)) + patch.write_token(APTokenTypes.COPY, 0x7FD9, (1, 0x3C020)) + + # handle palette + if world.options.kirby_flavor_preset.value != 0: + for addr in kirby_target_palettes: + target = kirby_target_palettes[addr] + palette = get_kirby_palette(world) + if palette is not None: + patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2])) + + if world.options.gooey_flavor_preset.value != 0: + for addr in gooey_target_palettes: + target = gooey_target_palettes[addr] + palette = get_gooey_palette(world) + if palette is not None: + patch.write_token(APTokenTypes.WRITE, addr, get_palette_bytes(palette, target[0], target[1], target[2])) + + patch.write_file("token_patch.bin", patch.get_token_binary()) + + +def get_base_rom_bytes() -> bytes: + rom_file: str = get_base_rom_path() + base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + base_rom_bytes = bytes(Utils.read_snes_rom(open(rom_file, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() not in {KDL3UHASH, KDL3JHASH}: + raise Exception("Supplied Base Rom does not match known MD5 for US or JP release. " + "Get the correct game and version, then dump it") + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options: settings.Settings = settings.get_settings() + if not file_name: + file_name = options["kdl3_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/kdl3/room.py b/worlds/kdl3/room.py new file mode 100644 index 000000000000..bcc1c7a709cb --- /dev/null +++ b/worlds/kdl3/room.py @@ -0,0 +1,133 @@ +import struct +from typing import Optional, Dict, TYPE_CHECKING, List, Union +from BaseClasses import Region, ItemClassification, MultiWorld +from worlds.Files import APTokenTypes +from .client_addrs import consumable_addrs, star_addrs + +if TYPE_CHECKING: + from .rom import KDL3ProcedurePatch + +animal_map = { + "Rick Spawn": 0, + "Kine Spawn": 1, + "Coo Spawn": 2, + "Nago Spawn": 3, + "ChuChu Spawn": 4, + "Pitch Spawn": 5 +} + + +class KDL3Room(Region): + pointer: int = 0 + level: int = 0 + stage: int = 0 + room: int = 0 + music: int = 0 + default_exits: List[Dict[str, Union[int, List[str]]]] + animal_pointers: List[int] + enemies: List[str] + entity_load: List[List[int]] + consumables: List[Dict[str, Union[int, str]]] + + def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str], level: int, + stage: int, room: int, pointer: int, music: int, + default_exits: List[Dict[str, List[str]]], + animal_pointers: List[int], enemies: List[str], + entity_load: List[List[int]], + consumables: List[Dict[str, Union[int, str]]], consumable_pointer: int) -> None: + super().__init__(name, player, multiworld, hint) + self.level = level + self.stage = stage + self.room = room + self.pointer = pointer + self.music = music + self.default_exits = default_exits + self.animal_pointers = animal_pointers + self.enemies = enemies + self.entity_load = entity_load + self.consumables = consumables + self.consumable_pointer = consumable_pointer + + def patch(self, patch: "KDL3ProcedurePatch", consumables: bool, local_items: bool) -> None: + patch.write_token(APTokenTypes.WRITE, self.pointer + 2, self.music.to_bytes(1, "little")) + animals = [x.item.name for x in self.locations if "Animal" in x.name and x.item] + if len(animals) > 0: + for current_animal, address in zip(animals, self.animal_pointers): + patch.write_token(APTokenTypes.WRITE, self.pointer + address + 7, + animal_map[current_animal].to_bytes(1, "little")) + if local_items: + for location in self.get_locations(): + if location.item is None or location.item.player != self.player: + continue + item = location.item.code + if item is None: + continue + item_idx = item & 0x00000F + location_idx = location.address & 0xFFFF + if location_idx & 0xF00 in (0x300, 0x400, 0x500, 0x600): + # consumable or star, need remapped + location_base = location_idx & 0xF00 + if location_base == 0x300: + # consumable + location_idx = consumable_addrs[location_idx & 0xFF] | 0x1000 + else: + # star + location_idx = star_addrs[location.address] | 0x2000 + if item & 0x000070 == 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x10])) + elif item & 0x000010 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x20])) + elif item & 0x000020 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x40])) + elif item & 0x000040 > 0: + patch.write_token(APTokenTypes.WRITE, 0x4B000 + location_idx, bytes([item_idx | 0x80])) + + if consumables: + load_len = len(self.entity_load) + for consumable in self.consumables: + location = next(x for x in self.locations if x.name == consumable["name"]) + assert location.item is not None + is_progression = location.item.classification & ItemClassification.progression + if load_len == 8: + # edge case, there is exactly 1 room with 8 entities and only 1 consumable among them + if not (any(x in self.entity_load for x in [[0, 22], [1, 22]]) + and any(x in self.entity_load for x in [[2, 22], [3, 22]])): + replacement_target = self.entity_load.index( + next(x for x in self.entity_load if x in [[0, 22], [1, 22], [2, 22], [3, 22]])) + if is_progression: + vtype = 0 + else: + vtype = 2 + patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (replacement_target * 2), + vtype.to_bytes(1, "little")) + self.entity_load[replacement_target] = [vtype, 22] + else: + if is_progression: + # we need to see if 1-ups are in our load list + if any(x not in self.entity_load for x in [[0, 22], [1, 22]]): + self.entity_load.append([0, 22]) + else: + if any(x not in self.entity_load for x in [[2, 22], [3, 22]]): + # edge case: if (1, 22) is in, we need to load (3, 22) instead + if [1, 22] in self.entity_load: + self.entity_load.append([3, 22]) + else: + self.entity_load.append([2, 22]) + if load_len < len(self.entity_load): + patch.write_token(APTokenTypes.WRITE, self.pointer + 88 + (load_len * 2), + bytes(self.entity_load[load_len])) + patch.write_token(APTokenTypes.WRITE, self.pointer + 104 + (load_len * 2), + bytes(struct.pack("H", self.consumable_pointer))) + if is_progression: + if [1, 22] in self.entity_load: + vtype = 1 + else: + vtype = 0 + else: + if [3, 22] in self.entity_load: + vtype = 3 + else: + vtype = 2 + assert isinstance(consumable["pointer"], int) + patch.write_token(APTokenTypes.WRITE, self.pointer + consumable["pointer"] + 7, + vtype.to_bytes(1, "little")) diff --git a/worlds/kdl3/Rules.py b/worlds/kdl3/rules.py similarity index 70% rename from worlds/kdl3/Rules.py rename to worlds/kdl3/rules.py index 6a85ef84f054..a08e99257e17 100644 --- a/worlds/kdl3/Rules.py +++ b/worlds/kdl3/rules.py @@ -1,7 +1,7 @@ from worlds.generic.Rules import set_rule, add_rule -from .Names import LocationName, EnemyAbilities -from .Locations import location_table -from .Options import GoalSpeed +from .names import location_name, enemy_abilities, animal_friend_spawns +from .locations import location_table +from .options import GoalSpeed import typing if typing.TYPE_CHECKING: @@ -10,9 +10,9 @@ def can_reach_boss(state: "CollectionState", player: int, level: int, open_world: int, - ow_boss_req: int, player_levels: typing.Dict[int, typing.Dict[int, int]]): + ow_boss_req: int, player_levels: typing.Dict[int, typing.List[int]]) -> bool: if open_world: - return state.has(f"{LocationName.level_names_inverse[level]} - Stage Completion", player, ow_boss_req) + return state.has(f"{location_name.level_names_inverse[level]} - Stage Completion", player, ow_boss_req) else: return state.can_reach(location_table[player_levels[level][5]], "Location", player) @@ -86,11 +86,11 @@ def can_reach_cutter(state: "CollectionState", player: int) -> bool: } -def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]): +def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool: # check animal requirements if not (can_reach_coo(state, player) and can_reach_kine(state, player)): return False - for abilities, bukisets in EnemyAbilities.enemy_restrictive[1:5]: + for abilities, bukisets in enemy_abilities.enemy_restrictive[1:5]: iterator = iter(x for x in bukisets if copy_abilities[x] in abilities) target_bukiset = next(iterator, None) can_reach = False @@ -103,7 +103,7 @@ def can_assemble_rob(state: "CollectionState", player: int, copy_abilities: typi return can_reach_parasol(state, player) and can_reach_stone(state, player) -def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]): +def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: typing.Dict[str, str]) -> bool: can_reach = True for enemy in {"Sparky", "Blocky", "Jumper Shoot", "Yuki", "Sir Kibble", "Haboki", "Boboo", "Captain Stitch"}: can_reach = can_reach & ability_map[copy_abilities[enemy]](state, player) @@ -112,114 +112,114 @@ def can_fix_angel_wings(state: "CollectionState", player: int, copy_abilities: t def set_rules(world: "KDL3World") -> None: # Level 1 - set_rule(world.multiworld.get_location(LocationName.grass_land_muchi, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_muchi, world.player), lambda state: can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_chao, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_chao, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_mine, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_mine, world.player), lambda state: can_reach_kine(state, world.player)) # Level 2 - set_rule(world.multiworld.get_location(LocationName.ripple_field_5, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_kamuribana, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_kamuribana, world.player), lambda state: can_reach_pitch(state, world.player) and can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_bakasa, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_bakasa, world.player), lambda state: can_reach_kine(state, world.player) and can_reach_parasol(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_toad, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_toad, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_mama_pitch, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_mama_pitch, world.player), lambda state: (can_reach_pitch(state, world.player) and can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) # Level 3 - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5, world.player), lambda state: can_reach_cutter(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_auntie, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_auntie, world.player), lambda state: can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_nyupun, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_nyupun, world.player), lambda state: can_reach_chuchu(state, world.player) and can_reach_cutter(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_rob, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_rob, world.player), lambda state: can_assemble_rob(state, world.player, world.copy_abilities) ) # Level 4 - set_rule(world.multiworld.get_location(LocationName.cloudy_park_hibanamodoki, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_hibanamodoki, world.player), lambda state: can_reach_coo(state, world.player) and can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_piyokeko, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_piyokeko, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_mikarin, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_mikarin, world.player), lambda state: can_reach_coo(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_pick, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_pick, world.player), lambda state: can_reach_rick(state, world.player)) # Level 5 - set_rule(world.multiworld.get_location(LocationName.iceberg_4, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_4, world.player), lambda state: can_reach_burning(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_kogoesou, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_kogoesou, world.player), lambda state: can_reach_burning(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_samus, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_samus, world.player), lambda state: can_reach_ice(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_name, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_name, world.player), lambda state: (can_reach_coo(state, world.player) and can_reach_burning(state, world.player) and can_reach_chuchu(state, world.player))) # ChuChu is guaranteed here, but we use this for consistency - set_rule(world.multiworld.get_location(LocationName.iceberg_shiro, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_shiro, world.player), lambda state: can_reach_nago(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.iceberg_angel, world.player), + set_rule(world.multiworld.get_location(location_name.iceberg_angel, world.player), lambda state: can_fix_angel_wings(state, world.player, world.copy_abilities)) # Consumables if world.options.consumables: - set_rule(world.multiworld.get_location(LocationName.grass_land_1_u1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_1_u1, world.player), lambda state: can_reach_parasol(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_1_m1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_1_m1, world.player), lambda state: can_reach_spark(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.grass_land_2_u1, world.player), + set_rule(world.multiworld.get_location(location_name.grass_land_2_u1, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_2_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_2_u1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_2_m1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_2_m1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_3_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_3_u1, world.player), lambda state: can_reach_cutter(state, world.player) or can_reach_spark(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_4_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_4_u1, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_4_m2, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_4_m2, world.player), lambda state: can_reach_stone(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_m1, world.player), lambda state: can_reach_kine(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_u1, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_u1, world.player), lambda state: (can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.ripple_field_5_m2, world.player), + set_rule(world.multiworld.get_location(location_name.ripple_field_5_m2, world.player), lambda state: (can_reach_kine(state, world.player) and can_reach_burning(state, world.player) and can_reach_stone(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_u1, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_4_u1, world.player), lambda state: can_reach_clean(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_4_m2, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_4_m2, world.player), lambda state: can_reach_needle(state, world.player)) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u2, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u2, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u3, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u3, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.sand_canyon_5_u4, world.player), + set_rule(world.multiworld.get_location(location_name.sand_canyon_5_u4, world.player), lambda state: can_reach_ice(state, world.player) and (can_reach_rick(state, world.player) or can_reach_coo(state, world.player) or can_reach_chuchu(state, world.player) or can_reach_pitch(state, world.player) or can_reach_nago(state, world.player))) - set_rule(world.multiworld.get_location(LocationName.cloudy_park_6_u1, world.player), + set_rule(world.multiworld.get_location(location_name.cloudy_park_6_u1, world.player), lambda state: can_reach_cutter(state, world.player)) if world.options.starsanity: @@ -274,50 +274,57 @@ def set_rules(world: "KDL3World") -> None: # copy ability access edge cases # Kirby cannot eat enemies fully submerged in water. Vast majority of cases, the enemy can be brought to the surface # and eaten by inhaling while falling on top of them - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_2_E3, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_2_E3, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_3_E6, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_3_E6, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) # Ripple Field 4 E5, E7, and E8 are doable, but too strict to leave in logic - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E5, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E5, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E7, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E7, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_4_E8, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_4_E8, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E1, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E1, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E2, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E2, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E3, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E3, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Ripple_Field_5_E4, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Ripple_Field_5_E4, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E7, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E7, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E8, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E8, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E9, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E9, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) - set_rule(world.multiworld.get_location(EnemyAbilities.Sand_Canyon_4_E10, world.player), + set_rule(world.multiworld.get_location(enemy_abilities.Sand_Canyon_4_E10, world.player), lambda state: can_reach_kine(state, world.player) or can_reach_chuchu(state, world.player)) + # animal friend rules + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a2, world.player), + lambda state: can_reach_coo(state, world.player) and can_reach_burning(state, world.player)) + set_rule(world.multiworld.get_location(animal_friend_spawns.iceberg_4_a3, world.player), + lambda state: can_reach_chuchu(state, world.player) and can_reach_coo(state, world.player) + and can_reach_burning(state, world.player)) + for boss_flag, purification, i in zip(["Level 1 Boss - Purified", "Level 2 Boss - Purified", "Level 3 Boss - Purified", "Level 4 Boss - Purified", "Level 5 Boss - Purified"], - [LocationName.grass_land_whispy, LocationName.ripple_field_acro, - LocationName.sand_canyon_poncon, LocationName.cloudy_park_ado, - LocationName.iceberg_dedede], + [location_name.grass_land_whispy, location_name.ripple_field_acro, + location_name.sand_canyon_poncon, location_name.cloudy_park_ado, + location_name.iceberg_dedede], range(1, 6)): set_rule(world.multiworld.get_location(boss_flag, world.player), - lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1]) - and can_reach_boss(state, world.player, i, + lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) + and can_reach_boss(state, world.player, x, world.options.open_world.value, world.options.ow_boss_requirement.value, world.player_levels))) set_rule(world.multiworld.get_location(purification, world.player), - lambda state, i=i: (state.has("Heart Star", world.player, world.boss_requirements[i - 1]) - and can_reach_boss(state, world.player, i, + lambda state, x=i: (state.has("Heart Star", world.player, world.boss_requirements[x - 1]) + and can_reach_boss(state, world.player, x, world.options.open_world.value, world.options.ow_boss_requirement.value, world.player_levels))) @@ -327,12 +334,12 @@ def set_rules(world: "KDL3World") -> None: for level in range(2, 6): set_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), - lambda state, i=level: state.has(f"Level {i - 1} Boss Defeated", world.player)) + lambda state, x=level: state.has(f"Level {x - 1} Boss Defeated", world.player)) if world.options.strict_bosses: for level in range(2, 6): add_rule(world.multiworld.get_entrance(f"To Level {level}", world.player), - lambda state, i=level: state.has(f"Level {i - 1} Boss Purified", world.player)) + lambda state, x=level: state.has(f"Level {x - 1} Boss Purified", world.player)) if world.options.goal_speed == GoalSpeed.option_normal: add_rule(world.multiworld.get_entrance("To Level 6", world.player), diff --git a/worlds/kdl3/data/APPauseIcons.dat b/worlds/kdl3/src/APPauseIcons.dat similarity index 100% rename from worlds/kdl3/data/APPauseIcons.dat rename to worlds/kdl3/src/APPauseIcons.dat diff --git a/worlds/kdl3/src/kdl3_basepatch.asm b/worlds/kdl3/src/kdl3_basepatch.asm index e419d0632f0e..95c85f032c55 100644 --- a/worlds/kdl3/src/kdl3_basepatch.asm +++ b/worlds/kdl3/src/kdl3_basepatch.asm @@ -58,6 +58,10 @@ org $01AFC8 org $01B013 SEC ; Remove Dedede Bad Ending +org $01B050 + JSL HookBossPurify + NOP + org $02B7B0 ; Zero unlock LDA $80A0 CMP #$0001 @@ -160,7 +164,6 @@ CopyAbilityAnimalOverride: STA $39DF, X RTL -org $079A00 HeartStarCheck: TXA CMP #$0000 ; is this level 1 @@ -201,7 +204,6 @@ HeartStarCheck: SEC RTL -org $079A80 OpenWorldUnlock: PHX LDX $900E ; Are we on open world? @@ -224,7 +226,6 @@ OpenWorldUnlock: PLX RTL -org $079B00 MainLoopHook: STA $D4 INC $3524 @@ -239,16 +240,18 @@ MainLoopHook: BEQ .Return ; return if we are LDA $5541 ; gooey status BPL .Slowness ; gooey is already spawned + LDA $39D1 ; is kirby alive? + BEQ .Slowness ; branch if he isn't + ; maybe BMI here too? LDA $8080 CMP #$0000 ; did we get a gooey trap BEQ .Slowness ; branch if we did not JSL GooeySpawn - STZ $8080 + DEC $8080 .Slowness: LDA $8082 ; slowness BEQ .Eject ; are we under the effects of a slowness trap - DEC - STA $8082 ; dec by 1 each frame + DEC $8082 ; dec by 1 each frame .Eject: PHX PHY @@ -258,14 +261,13 @@ MainLoopHook: BEQ .PullVars ; branch if we haven't received eject LDA #$2000 ; select button press STA $60C1 ; write to controller mirror - STZ $8084 + DEC $8084 .PullVars: PLY PLX .Return: RTL -org $079B80 HeartStarGraphicFix: LDA #$0000 PHX @@ -288,7 +290,7 @@ HeartStarGraphicFix: ASL TAX LDA $07D080, X ; table of original stage number - CMP #$0003 ; is the current stage a minigame stage? + CMP #$0002 ; is the current stage a minigame stage? BEQ .ReturnTrue ; branch if so CLC BRA .Return @@ -299,7 +301,6 @@ HeartStarGraphicFix: PLX RTL -org $079BF0 ParseItemQueue: ; Local item queue parsing NOP @@ -336,8 +337,6 @@ ParseItemQueue: AND #$000F ASL TAY - LDA $8080,Y - BNE .LoopCheck JSL .ApplyNegative RTL .ApplyAbility: @@ -418,35 +417,73 @@ ParseItemQueue: CPY #$0005 BCS .PlayNone LDA $8080,Y - BNE .Return + CPY #$0002 + BNE .Increment + CLC LDA #$0384 + ADC $8080, Y + BVC .PlayNegative + LDA #$FFFF + .PlayNegative: STA $8080,Y LDA #$00A7 BRA .PlaySFXLong + .Increment: + INC + STA $8080, Y + BRA .PlayNegative .PlayNone: LDA #$0000 BRA .PlaySFXLong -org $079D00 AnimalFriendSpawn: PHA CPX #$0002 ; is this an animal friend? BNE .Return XBA PHA + PHX + PHA + LDX #$0000 + .CheckSpawned: + LDA $05CA, X + BNE .Continue + LDA #$0002 + CMP $074A, X + BNE .ContinueCheck + PLA + PHA + XBA + CMP $07CA, X + BEQ .AlreadySpawned + .ContinueCheck: + INX + INX + BRA .CheckSpawned + .Continue: + PLA + PLX ASL TAY PLA INC CMP $8000, Y ; do we have this animal friend BEQ .Return ; we have this animal friend + .False: INX .Return: PLY LDA #$9999 RTL + .AlreadySpawned: + PLA + PLX + ASL + TAY + PLA + BRA .False + -org $079E00 WriteBWRAM: LDY #$6001 ;starting addr LDA #$1FFE ;bytes to write @@ -479,7 +516,6 @@ WriteBWRAM: .Return: RTL -org $079E80 ConsumableSet: PHA PHX @@ -507,7 +543,6 @@ ConsumableSet: ASL TAX LDA $07D020, X ; current stage - DEC ASL #6 TAX PLA @@ -519,8 +554,16 @@ ConsumableSet: BRA .LoopHead ; return to loop head .ApplyCheck: LDA $A000, X ; consumables index + PHA ORA #$0001 STA $A000, X + PLA + AND #$00FF + BNE .Return + TXA + ORA #$1000 + JSL ApplyLocalCheck + .Return: PLY PLX PLA @@ -528,7 +571,6 @@ ConsumableSet: AND #$00FF RTL -org $079F00 NormalGoalSet: PHX LDA $07D012 @@ -549,7 +591,6 @@ NormalGoalSet: STA $5AC1 ; cutscene RTL -org $079F80 FinalIcebergFix: PHX PHY @@ -572,7 +613,7 @@ FinalIcebergFix: ASL TAX LDA $07D020, X - CMP #$001E + CMP #$001D BEQ .ReturnTrue CLC BRA .Return @@ -583,7 +624,6 @@ FinalIcebergFix: PLX RTL -org $07A000 StrictBosses: PHX LDA $901E ; Do we have strict bosses enabled? @@ -610,7 +650,6 @@ StrictBosses: LDA $53CD RTL -org $07A030 NintenHalken: LDX #$0005 .Halken: @@ -628,7 +667,6 @@ NintenHalken: LDA #$0001 RTL -org $07A080 StageCompleteSet: PHX LDA $5AC1 ; completed stage cutscene @@ -656,9 +694,17 @@ StageCompleteSet: ASL TAX LDA $9020, X ; load the stage we completed - DEC ASL TAX + PHX + LDA $8200, X + AND #$00FF + BNE .ApplyClear + TXA + LSR + JSL ApplyLocalCheck + .ApplyClear: + PLX LDA #$0001 ORA $8200, X STA $8200, X @@ -668,7 +714,6 @@ StageCompleteSet: CMP $53CB RTL -org $07A100 OpenWorldBossUnlock: PHX PHY @@ -699,7 +744,6 @@ OpenWorldBossUnlock: .LoopStage: PLX LDY $9020, X ; get stage id - DEY INX INX PHA @@ -732,7 +776,6 @@ OpenWorldBossUnlock: PLX RTL -org $07A180 GooeySpawn: PHY PHX @@ -768,7 +811,6 @@ GooeySpawn: PLY RTL -org $07A200 SpeedTrap: PHX LDX $8082 ; do we have slowness @@ -780,7 +822,6 @@ SpeedTrap: EOR #$FFFF RTL -org $07A280 HeartStarVisual: CPX #$0000 BEQ .SkipInx @@ -844,7 +885,6 @@ HeartStarVisual: .Return: RTL -org $07A300 LoadFont: JSL $00D29F ; play sfx PHX @@ -915,7 +955,6 @@ LoadFont: PLX RTL -org $07A380 HeartStarVisual2: LDA #$2C80 STA $0000, Y @@ -1029,14 +1068,12 @@ HeartStarVisual2: STA $0000, Y RTL -org $07A480 HeartStarSelectFix: PHX TXA ASL TAX LDA $9020, X - DEC TAX .LoopHead: CMP #$0006 @@ -1051,15 +1088,31 @@ HeartStarSelectFix: AND #$00FF RTL -org $07A500 HeartStarCutsceneFix: TAX LDA $53D3 DEC STA $5AC3 + LDA $53A7, X + AND #$00FF + BNE .Return + PHX + TXA + .Loop: + CMP #$0007 + BCC .Continue + SEC + SBC #$0007 + DEX + BRA .Loop + .Continue: + TXA + ORA #$0100 + JSL ApplyLocalCheck + PLX + .Return RTL -org $07A510 GiftGiving: CMP #$0008 .This: @@ -1075,7 +1128,6 @@ GiftGiving: PLX JML $CABC18 -org $07A550 PauseMenu: JSL $00D29F PHX @@ -1136,7 +1188,6 @@ PauseMenu: PLX RTL -org $07A600 StarsSet: PHA PHX @@ -1166,7 +1217,6 @@ StarsSet: ASL TAX LDA $07D020, X - DEC ASL ASL ASL @@ -1183,8 +1233,15 @@ StarsSet: BRA .2LoopHead .2LoopEnd: LDA $B000, X + PHA ORA #$0001 STA $B000, X + PLA + AND #$00FF + BNE .Return + TXA + ORA #$2000 + JSL ApplyLocalCheck .Return: PLY PLX @@ -1199,6 +1256,48 @@ StarsSet: STA $39D7 BRA .Return +ApplyLocalCheck: +; args: A-address of check following $08B000 + TAX + LDA $09B000, X + AND #$00FF + TAY + LDX #$0000 + .Loop: + LDA $C000, X + BEQ .Apply + INX + INX + CPX #$0010 + BCC .Loop + BRA .Return ; this is dangerous, could lose a check here + .Apply: + TYA + STA $C000, X + .Return: + RTL + +HookBossPurify: + ORA $B0 + STA $53D5 + LDA $B0 + LDX #$0000 + LSR + .Loop: + BIT #$0001 + BNE .Apply + LSR + LSR + INX + CPX #$0005 + BCS .Return + BRA .Loop + .Apply: + TXA + ORA #$0200 + JSL ApplyLocalCheck + .Return: + RTL org $07C000 db "KDL3_BASEPATCH_ARCHI" @@ -1234,4 +1333,7 @@ org $07E040 db $3A, $01 db $3B, $05 db $3C, $05 - db $3D, $05 \ No newline at end of file + db $3D, $05 + +org $07F000 +incbin "APPauseIcons.dat" \ No newline at end of file diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py index 4d3f4d70faae..92f1d7261f1f 100644 --- a/worlds/kdl3/test/__init__.py +++ b/worlds/kdl3/test/__init__.py @@ -6,6 +6,8 @@ from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all +# mypy: ignore-errors +# This is a copy of core code, and I'm not smart enough to solve the errors in here class KDL3TestBase(WorldTestBase): diff --git a/worlds/kdl3/test/test_goal.py b/worlds/kdl3/test/test_goal.py index ce53642a9716..2c6ae614d4aa 100644 --- a/worlds/kdl3/test/test_goal.py +++ b/worlds/kdl3/test/test_goal.py @@ -5,12 +5,12 @@ class TestFastGoal(KDL3TestBase): options = { "open_world": False, "goal_speed": "fast", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, } - def test_goal(self): + def test_goal(self) -> None: self.assertBeatable(False) heart_stars = self.get_items_by_name("Heart Star") self.collect(heart_stars[0:14]) @@ -30,12 +30,12 @@ class TestNormalGoal(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, } - def test_goal(self): + def test_goal(self) -> None: self.assertBeatable(False) heart_stars = self.get_items_by_name("Heart Star") self.collect(heart_stars[0:14]) @@ -51,14 +51,14 @@ def test_goal(self): self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) self.assertBeatable(True) - def test_kine(self): + def test_kine(self) -> None: self.collect_by_name(["Cutter", "Burning", "Heart Star"]) self.assertBeatable(False) - def test_cutter(self): + def test_cutter(self) -> None: self.collect_by_name(["Kine", "Burning", "Heart Star"]) self.assertBeatable(False) - def test_burning(self): + def test_burning(self) -> None: self.collect_by_name(["Cutter", "Kine", "Heart Star"]) self.assertBeatable(False) diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index bde9abc409ac..024f1b11a591 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -1,6 +1,6 @@ from . import KDL3TestBase +from ..names import location_name from Options import PlandoConnection -from ..Names import LocationName import typing @@ -12,31 +12,31 @@ class TestLocations(KDL3TestBase): # these ensure we can always reach all stages physically } - def test_simple_heart_stars(self): - self.run_location_test(LocationName.grass_land_muchi, ["ChuChu"]) - self.run_location_test(LocationName.grass_land_chao, ["Stone"]) - self.run_location_test(LocationName.grass_land_mine, ["Kine"]) - self.run_location_test(LocationName.ripple_field_kamuribana, ["Pitch", "Clean"]) - self.run_location_test(LocationName.ripple_field_bakasa, ["Kine", "Parasol"]) - self.run_location_test(LocationName.ripple_field_toad, ["Needle"]) - self.run_location_test(LocationName.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) - self.run_location_test(LocationName.sand_canyon_auntie, ["Clean"]) - self.run_location_test(LocationName.sand_canyon_nyupun, ["ChuChu", "Cutter"]) - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]), - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]), - self.run_location_test(LocationName.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]), - self.run_location_test(LocationName.cloudy_park_hibanamodoki, ["Coo", "Clean"]) - self.run_location_test(LocationName.cloudy_park_piyokeko, ["Needle"]) - self.run_location_test(LocationName.cloudy_park_mikarin, ["Coo"]) - self.run_location_test(LocationName.cloudy_park_pick, ["Rick"]) - self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"]) - self.run_location_test(LocationName.iceberg_samus, ["Ice"]) - self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"]) - self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", + def test_simple_heart_stars(self) -> None: + self.run_location_test(location_name.grass_land_muchi, ["ChuChu"]) + self.run_location_test(location_name.grass_land_chao, ["Stone"]) + self.run_location_test(location_name.grass_land_mine, ["Kine"]) + self.run_location_test(location_name.ripple_field_kamuribana, ["Pitch", "Clean"]) + self.run_location_test(location_name.ripple_field_bakasa, ["Kine", "Parasol"]) + self.run_location_test(location_name.ripple_field_toad, ["Needle"]) + self.run_location_test(location_name.ripple_field_mama_pitch, ["Pitch", "Kine", "Burning", "Stone"]) + self.run_location_test(location_name.sand_canyon_auntie, ["Clean"]) + self.run_location_test(location_name.sand_canyon_nyupun, ["ChuChu", "Cutter"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Ice"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Ice"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Spark", "Needle"]) + self.run_location_test(location_name.sand_canyon_rob, ["Stone", "Kine", "Coo", "Parasol", "Clean", "Needle"]) + self.run_location_test(location_name.cloudy_park_hibanamodoki, ["Coo", "Clean"]) + self.run_location_test(location_name.cloudy_park_piyokeko, ["Needle"]) + self.run_location_test(location_name.cloudy_park_mikarin, ["Coo"]) + self.run_location_test(location_name.cloudy_park_pick, ["Rick"]) + self.run_location_test(location_name.iceberg_kogoesou, ["Burning"]) + self.run_location_test(location_name.iceberg_samus, ["Ice"]) + self.run_location_test(location_name.iceberg_name, ["Burning", "Coo", "ChuChu"]) + self.run_location_test(location_name.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", "Stone", "Ice"]) - def run_location_test(self, location: str, itempool: typing.List[str]): + def run_location_test(self, location: str, itempool: typing.List[str]) -> None: items = itempool.copy() while len(itempool) > 0: self.assertFalse(self.can_reach_location(location), str(self.multiworld.seed)) @@ -57,7 +57,7 @@ class TestShiro(KDL3TestBase): "plando_options": "connections" } - def test_shiro(self): + def test_shiro(self) -> None: self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) self.collect_by_name("Nago") self.assertFalse(self.can_reach_location("Iceberg 5 - Shiro"), str(self.multiworld.seed)) diff --git a/worlds/kdl3/test/test_shuffles.py b/worlds/kdl3/test/test_shuffles.py index d676b641b056..3ba376d068e6 100644 --- a/worlds/kdl3/test/test_shuffles.py +++ b/worlds/kdl3/test/test_shuffles.py @@ -1,47 +1,61 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional from . import KDL3TestBase -from ..Room import KDL3Room +from ..room import KDL3Room +from ..names import animal_friend_spawns class TestCopyAbilityShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "copy_ability_randomization": "enabled", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) - - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) - - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) - - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) - - def test_cutter_and_burning_reachable(self): + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) @@ -63,7 +77,7 @@ def test_cutter_and_burning_reachable(self): else: self.fail("Could not reach Burning Ability before Iceberg 4!") - def test_valid_abilities_for_ROB(self): + def test_valid_abilities_for_ROB(self) -> None: # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach # first we need to identify our bukiset requirements @@ -74,13 +88,13 @@ def test_valid_abilities_for_ROB(self): ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ] copy_abilities = self.multiworld.worlds[1].copy_abilities - required_abilities: List[Tuple[str]] = [] + required_abilities: List[List[str]] = [] for abilities, bukisets in groups: potential_abilities: List[str] = list() for bukiset in bukisets: if copy_abilities[bukiset] in abilities: potential_abilities.append(copy_abilities[bukiset]) - required_abilities.append(tuple(potential_abilities)) + required_abilities.append(potential_abilities) collected_abilities = list() for group in required_abilities: self.assertFalse(len(group) == 0, str(self.multiworld.seed)) @@ -103,91 +117,147 @@ class TestAnimalShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "animal_randomization": "full", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) - - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) - - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) - - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) - - def test_locked_animals(self): - self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") - self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") - self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"}) + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_locked_animals(self) -> None: + ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1) + self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn", + f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}") + iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1) + self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn", + f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}") + sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1) + self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in + {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}, " + f"Seed: {self.multiworld.seed}") class TestAllShuffle(KDL3TestBase): options = { "open_world": False, "goal_speed": "normal", - "total_heart_stars": 30, + "max_heart_stars": 30, "heart_stars_required": 50, "filler_percentage": 0, "animal_randomization": "full", "copy_ability_randomization": "enabled", } - def test_goal(self): - self.assertBeatable(False) - heart_stars = self.get_items_by_name("Heart Star") - self.collect(heart_stars[0:14]) - self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect(heart_stars[14:15]) - self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) - self.assertBeatable(False) - self.collect_by_name(["Burning", "Cutter", "Kine"]) - self.assertBeatable(True) - self.remove([self.get_item_by_name("Love-Love Rod")]) - self.collect(heart_stars) - self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) - self.assertBeatable(True) - - def test_kine(self): - self.collect_by_name(["Cutter", "Burning", "Heart Star"]) - self.assertBeatable(False) - - def test_cutter(self): - self.collect_by_name(["Kine", "Burning", "Heart Star"]) - self.assertBeatable(False) - - def test_burning(self): - self.collect_by_name(["Cutter", "Kine", "Heart Star"]) - self.assertBeatable(False) - - def test_locked_animals(self): - self.assertTrue(self.multiworld.get_location("Ripple Field 5 - Animal 2", 1).item.name == "Pitch Spawn") - self.assertTrue(self.multiworld.get_location("Iceberg 4 - Animal 1", 1).item.name == "ChuChu Spawn") - self.assertTrue(self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1).item.name in {"Kine Spawn", "Coo Spawn"}) - - def test_cutter_and_burning_reachable(self): + def test_goal(self) -> None: + try: + self.assertBeatable(False) + heart_stars = self.get_items_by_name("Heart Star") + self.collect(heart_stars[0:14]) + self.assertEqual(self.count("Heart Star"), 14, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect(heart_stars[14:15]) + self.assertEqual(self.count("Heart Star"), 15, str(self.multiworld.seed)) + self.assertBeatable(False) + self.collect_by_name(["Burning", "Cutter", "Kine"]) + self.assertBeatable(True) + self.remove([self.get_item_by_name("Love-Love Rod")]) + self.collect(heart_stars) + self.assertEqual(self.count("Heart Star"), 30, str(self.multiworld.seed)) + self.assertBeatable(True) + except AssertionError as ex: + # if assert beatable fails, this will catch and print the seed + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_kine(self) -> None: + try: + self.collect_by_name(["Cutter", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_cutter(self) -> None: + try: + self.collect_by_name(["Kine", "Burning", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_burning(self) -> None: + try: + self.collect_by_name(["Cutter", "Kine", "Heart Star"]) + self.assertBeatable(False) + except AssertionError as ex: + raise AssertionError(f"Test failed, Seed:{self.multiworld.seed}") from ex + + def test_locked_animals(self) -> None: + ripple_field_5 = self.multiworld.get_location("Ripple Field 5 - Animal 2", 1) + self.assertTrue(ripple_field_5.item is not None and ripple_field_5.item.name == "Pitch Spawn", + f"Multiworld did not place Pitch, Seed: {self.multiworld.seed}") + iceberg_4 = self.multiworld.get_location("Iceberg 4 - Animal 1", 1) + self.assertTrue(iceberg_4.item is not None and iceberg_4.item.name == "ChuChu Spawn", + f"Multiworld did not place ChuChu, Seed: {self.multiworld.seed}") + sand_canyon_6 = self.multiworld.get_location("Sand Canyon 6 - Animal 1", 1) + self.assertTrue(sand_canyon_6.item is not None and sand_canyon_6.item.name in + {"Kine Spawn", "Coo Spawn"}, f"Multiworld did not place Coo/Kine, Seed: {self.multiworld.seed}") + + def test_problematic(self) -> None: + for spawns in animal_friend_spawns.problematic_sets: + placed = [self.multiworld.get_location(spawn, 1).item for spawn in spawns] + placed_names = set([item.name for item in placed]) + self.assertEqual(len(placed), len(placed_names), + f"Duplicate animal placed in problematic locations:" + f" {[spawn.location for spawn in placed]}, " + f"Seed: {self.multiworld.seed}") + + def test_cutter_and_burning_reachable(self) -> None: rooms = self.multiworld.worlds[1].rooms copy_abilities = self.multiworld.worlds[1].copy_abilities sand_canyon_5 = self.multiworld.get_region("Sand Canyon 5 - 9", 1) @@ -209,7 +279,7 @@ def test_cutter_and_burning_reachable(self): else: self.fail("Could not reach Burning Ability before Iceberg 4!") - def test_valid_abilities_for_ROB(self): + def test_valid_abilities_for_ROB(self) -> None: # there exists a subset of 4-7 abilities that will allow us access to ROB heart star on default settings self.collect_by_name(["Heart Star", "Kine", "Coo"]) # we will guaranteed need Coo, Kine, and Heart Stars to reach # first we need to identify our bukiset requirements @@ -220,13 +290,13 @@ def test_valid_abilities_for_ROB(self): ({"Stone Ability", "Burning Ability"}, {'Bukiset (Stone)', 'Bukiset (Burning)'}), ] copy_abilities = self.multiworld.worlds[1].copy_abilities - required_abilities: List[Tuple[str]] = [] + required_abilities: List[List[str]] = [] for abilities, bukisets in groups: potential_abilities: List[str] = list() for bukiset in bukisets: if copy_abilities[bukiset] in abilities: potential_abilities.append(copy_abilities[bukiset]) - required_abilities.append(tuple(potential_abilities)) + required_abilities.append(potential_abilities) collected_abilities = list() for group in required_abilities: self.assertFalse(len(group) == 0, str(self.multiworld.seed)) @@ -242,4 +312,4 @@ def test_valid_abilities_for_ROB(self): self.collect_by_name(["Cutter"]) self.assertTrue(self.can_reach_location("Sand Canyon 6 - Professor Hector & R.O.B"), - ''.join(str(self.multiworld.seed)).join(collected_abilities)) + f"Seed: {self.multiworld.seed}, Collected: {collected_abilities}") diff --git a/worlds/kh1/Client.py b/worlds/kh1/Client.py index acfd5dba3825..33fba85f6c54 100644 --- a/worlds/kh1/Client.py +++ b/worlds/kh1/Client.py @@ -31,6 +31,9 @@ def check_stdin() -> None: print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.") class KH1ClientCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx): + super().__init__(ctx) + def _cmd_deathlink(self): """Toggles Deathlink""" global death_link @@ -40,6 +43,40 @@ def _cmd_deathlink(self): else: death_link = True self.output(f"Death Link turned on") + + def _cmd_goal(self): + """Prints goal setting""" + if "goal" in self.ctx.slot_data.keys(): + self.output(str(self.ctx.slot_data["goal"])) + else: + self.output("Unknown") + + def _cmd_eotw_unlock(self): + """Prints End of the World Unlock setting""" + if "required_reports_door" in self.ctx.slot_data.keys(): + if self.ctx.slot_data["required_reports_door"] > 13: + self.output("Item") + else: + self.output(str(self.ctx.slot_data["required_reports_eotw"]) + " reports") + else: + self.output("Unknown") + + def _cmd_door_unlock(self): + """Prints Final Rest Door Unlock setting""" + if "door" in self.ctx.slot_data.keys(): + if self.ctx.slot_data["door"] == "reports": + self.output(str(self.ctx.slot_data["required_reports_door"]) + " reports") + else: + self.output(str(self.ctx.slot_data["door"])) + else: + self.output("Unknown") + + def _cmd_advanced_logic(self): + """Prints advanced logic setting""" + if "advanced_logic" in self.ctx.slot_data.keys(): + self.output(str(self.ctx.slot_data["advanced_logic"])) + else: + self.output("Unknown") class KH1Context(CommonContext): command_processor: int = KH1ClientCommandProcessor @@ -51,6 +88,8 @@ def __init__(self, server_address, password): self.send_index: int = 0 self.syncing = False self.awaiting_bridge = False + self.hinted_synth_location_ids = False + self.slot_data = {} # self.game_communication_path: files go in this path to pass data between us and the actual game if "localappdata" in os.environ: self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM") @@ -104,6 +143,7 @@ def on_package(self, cmd: str, args: dict): f.close() #Handle Slot Data + self.slot_data = args['slot_data'] for key in list(args['slot_data'].keys()): with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f: f.write(str(args['slot_data'][key])) @@ -217,11 +257,13 @@ async def game_watcher(ctx: KH1Context): if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10: await ctx.send_death(death_text = "Sora was defeated!") if file.find("insynthshop") > -1: - await ctx.send_msgs([{ - "cmd": "LocationScouts", - "locations": [2656401,2656402,2656403,2656404,2656405,2656406], - "create_as_hint": 2 - }]) + if not ctx.hinted_synth_location_ids: + await ctx.send_msgs([{ + "cmd": "LocationScouts", + "locations": [2656401,2656402,2656403,2656404,2656405,2656406], + "create_as_hint": 2 + }]) + ctx.hinted_synth_location_ids = True ctx.locations_checked = sending message = [{"cmd": 'LocationChecks', "locations": sending}] await ctx.send_msgs(message) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index 9f4784f74995..1f9358a4f5a6 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -83,8 +83,8 @@ class ItemName: RUPEES_200 = "200 Rupees" RUPEES_500 = "500 Rupees" SEASHELL = "Seashell" - MESSAGE = "Master Stalfos' Message" - GEL = "Gel" + MESSAGE = "Nothing" + GEL = "Zol Attack" BOOMERANG = "Boomerang" HEART_PIECE = "Heart Piece" BOWWOW = "BowWow" diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index e6f608a92180..69e856f3541b 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -280,6 +280,8 @@ def gen_hint(): name = "Your" else: name = f"{world.multiworld.player_name[location.item.player]}'s" + # filter out { and } since they cause issues with string.format later on + name = name.replace("{", "").replace("}", "") if isinstance(location, LinksAwakeningLocation): location_name = location.ladxr_item.metadata.name @@ -288,7 +290,9 @@ def gen_hint(): hint = f"{name} {location.item} is at {location_name}" if location.player != world.player: - hint += f" in {world.multiworld.player_name[location.player]}'s world" + # filter out { and } since they cause issues with string.format later on + player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "") + hint += f" in {player_name}'s world" # Cap hint size at 85 # Realistically we could go bigger but let's be safe instead diff --git a/worlds/ladx/LADXR/locations/shop.py b/worlds/ladx/LADXR/locations/shop.py index b68726665f5a..bee053716a04 100644 --- a/worlds/ladx/LADXR/locations/shop.py +++ b/worlds/ladx/LADXR/locations/shop.py @@ -18,7 +18,8 @@ def patch(self, rom, option, *, multiworld=None): mw_text = "" if multiworld: mw_text = f" for player {rom.player_names[multiworld - 1].encode('ascii', 'replace').decode()}" - + # filter out { and } since they cause issues with string.format later on + mw_text = mw_text.replace("{", "").replace("}", "") if self.custom_item_name: name = self.custom_item_name diff --git a/worlds/ladx/LADXR/patches/goldenLeaf.py b/worlds/ladx/LADXR/patches/goldenLeaf.py index 87cefae0f6d8..b35c722a4316 100644 --- a/worlds/ladx/LADXR/patches/goldenLeaf.py +++ b/worlds/ladx/LADXR/patches/goldenLeaf.py @@ -29,6 +29,7 @@ def fixGoldenLeaf(rom): rom.patch(0x03, 0x0980, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard rom.patch(0x06, 0x0059, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # If leaves >= 6 move richard rom.patch(0x06, 0x007D, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Richard message if no leaves - rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores FF in the leaf counter if we opened the path + rom.patch(0x06, 0x00B6, ASM("ld a, $FF"), ASM("ld a, $06")) + rom.patch(0x06, 0x00B8, ASM("ld [$DB15], a"), ASM("ld [wGoldenLeaves], a")) # Stores 6 in the leaf counter if we opened the path (instead of FF, so that nothing breaks if we get more for some reason) # 6:40EE uses leaves == 6 to check if we have collected the key, but only to change the message. # rom.patch(0x06, 0x2AEF, ASM("ld a, [$DB15]"), ASM("ld a, [wGoldenLeaves]")) # Telephone message handler diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py index 851fca164453..5f48b64c4f5e 100644 --- a/worlds/ladx/Tracker.py +++ b/worlds/ladx/Tracker.py @@ -149,6 +149,8 @@ class MagpieBridge: item_tracker = None ws = None features = [] + slot_data = {} + async def handler(self, websocket): self.ws = websocket while True: @@ -163,6 +165,9 @@ async def handler(self, websocket): await self.send_all_inventory() if "checks" in self.features: await self.send_all_checks() + if "slot_data" in self.features: + await self.send_slot_data(self.slot_data) + # Translate renamed IDs back to LADXR IDs @staticmethod def fixup_id(the_id): @@ -222,6 +227,18 @@ async def send_gps(self, gps): return await gps.send_location(self.ws) + async def send_slot_data(self, slot_data): + if not self.ws: + return + + logger.debug("Sending slot_data to magpie.") + message = { + "type": "slot_data", + "slot_data": slot_data + } + + await self.ws.send(json.dumps(message)) + async def serve(self): async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger): await asyncio.Future() # run forever @@ -237,4 +254,3 @@ async def set_item_tracker(self, item_tracker): await self.send_all_inventory() else: await self.send_inventory_diffs() - diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index c958ef212fe4..2846b40e67d9 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -216,7 +216,7 @@ def create_items(self) -> None: for _ in range(count): if item_name in exclude: exclude.remove(item_name) # this is destructive. create unique list above - self.multiworld.itempool.append(self.create_item("Master Stalfos' Message")) + self.multiworld.itempool.append(self.create_item("Nothing")) else: item = self.create_item(item_name) @@ -512,3 +512,34 @@ def remove(self, state, item: Item) -> bool: if change and item.name in self.rupees: state.prog_items[self.player]["RUPEES"] -= self.rupees[item.name] return change + + def get_filler_item_name(self) -> str: + return "Nothing" + + def fill_slot_data(self): + slot_data = {} + + if not self.multiworld.is_race: + # all of these option are NOT used by the LADX- or Text-Client. + # they are used by Magpie tracker (https://github.com/kbranch/Magpie/wiki/Autotracker-API) + # for convenient auto-tracking of the generated settings and adjusting the tracker accordingly + + slot_options = ["instrument_count"] + + slot_options_display_name = [ + "goal", "logic", "tradequest", "rooster", + "experimental_dungeon_shuffle", "experimental_entrance_shuffle", "trendy_game", "gfxmod", + "shuffle_nightmare_keys", "shuffle_small_keys", "shuffle_maps", + "shuffle_compasses", "shuffle_stone_beaks", "shuffle_instruments", "nag_messages" + ] + + # use the default behaviour to grab options + slot_data = self.options.as_dict(*slot_options) + + # for options which should not get the internal int value but the display name use the extra handling + slot_data.update({ + option: value.current_key + for option, value in dataclasses.asdict(self.options).items() if option in slot_options_display_name + }) + + return slot_data diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 16a1573b1d56..bbed1464530b 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -482,7 +482,9 @@ Crossroads: door: Crossroads Entrance The Tenacious: - door: Tenacious Entrance + - door: Tenacious Entrance + - room: The Tenacious + door: Shortcut to Hub Room Near Far Area: True Hedge Maze: door: Shortcut to Hedge Maze diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index e2d3d06bec96..789fc0856d62 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index a03c33c2f7b6..9a38953ffbdf 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -19,7 +19,7 @@ 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) ) @@ -27,6 +27,7 @@ class MessengerSettings(Group): class GamePath(FilePath): description = "The Messenger game executable" is_exe = True + md5s = ["1b53534569060bc06179356cd968ed1d"] game_path: GamePath = GamePath("TheMessenger.exe") diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 9fd08e52d899..77a0f634326c 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -1,10 +1,10 @@ +import argparse import io import logging import os.path import subprocess import urllib.request from shutil import which -from tkinter.messagebox import askyesnocancel from typing import Any, Optional from zipfile import ZipFile from Utils import open_file @@ -17,11 +17,33 @@ MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" -def launch_game(url: Optional[str] = None) -> None: +def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: + """ + Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. + + :param title: Title to be displayed at the top of the message box. + :param text: Text to be displayed inside the message box. + :return: Returns True if yes, False if no, None if cancel. + """ + from tkinter import Tk, messagebox + root = Tk() + root.withdraw() + ret = messagebox.askyesnocancel(title, text) + root.update() + return ret + + + +def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: """Check if Courier is installed""" - return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll")) + assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll") + with open(assembly_path, "rb") as assembly: + for line in assembly: + if b"Courier" in line: + return True + return False def mod_installed() -> bool: """Check if the mod is installed""" @@ -56,27 +78,34 @@ def install_courier() -> None: if not is_windows: mono_exe = which("mono") if not mono_exe: - # steam deck support but doesn't currently work - messagebox("Failure", "Failed to install Courier", True) - raise RuntimeError("Failed to install Courier") - # # download and use mono kickstart - # # this allows steam deck support - # mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip" - # target = os.path.join(folder, "monoKickstart") - # os.makedirs(target, exist_ok=True) - # with urllib.request.urlopen(mono_kick_url) as download: - # with ZipFile(io.BytesIO(download.read()), "r") as zf: - # for member in zf.infolist(): - # zf.extract(member, path=target) - # installer = subprocess.Popen([os.path.join(target, "precompiled"), - # os.path.join(folder, "MiniInstaller.exe")], shell=False) - # os.remove(target) + # download and use mono kickstart + # this allows steam deck support + mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip" + files = [] + with urllib.request.urlopen(mono_kick_url) as download: + with ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + if "precompiled/" not in member.filename or member.filename.endswith("/"): + continue + member.filename = member.filename.split("/")[-1] + if member.filename.endswith("bin.x86_64"): + member.filename = "MiniInstaller.bin.x86_64" + zf.extract(member, path=game_folder) + files.append(member.filename) + mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64") + os.chmod(mono_installer, 0o755) + installer = subprocess.Popen(mono_installer, shell=False) + failure = installer.wait() + for file in files: + os.remove(file) else: - installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False) + installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True) + failure = installer.wait() else: - installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False) + installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True) + failure = installer.wait() - failure = installer.wait() + print(failure) if failure: messagebox("Failure", "Failed to install Courier", True) os.chdir(working_directory) @@ -124,18 +153,35 @@ def available_mod_update(latest_version: str) -> bool: return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version) from . import MessengerWorld - game_folder = os.path.dirname(MessengerWorld.settings.game_path) + try: + game_folder = os.path.dirname(MessengerWorld.settings.game_path) + except ValueError as e: + logging.error(e) + messagebox("Invalid File", "Selected file did not match expected hash. " + "Please try again and ensure you select The Messenger.exe.") + return working_directory = os.getcwd() + # setup ssl context + try: + import certifi + import ssl + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + context.set_alpn_protocols(["http/1.1"]) + https_handler = urllib.request.HTTPSHandler(context=context) + opener = urllib.request.build_opener(https_handler) + urllib.request.install_opener(opener) + except ImportError: + pass if not courier_installed(): - should_install = askyesnocancel("Install Courier", - "No Courier installation detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Courier", + "No Courier installation detected. Would you like to install now?") if not should_install: return logging.info("Installing Courier") install_courier() if not mod_installed(): - should_install = askyesnocancel("Install Mod", - "No randomizer mod detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Mod", + "No randomizer mod detected. Would you like to install now?") if not should_install: return logging.info("Installing Mod") @@ -143,22 +189,33 @@ def available_mod_update(latest_version: str) -> bool: else: latest = request_data(MOD_URL)["tag_name"] if available_mod_update(latest): - should_update = askyesnocancel("Update Mod", - f"New mod version detected. Would you like to update to {latest} now?") + should_update = ask_yes_no_cancel("Update Mod", + f"New mod version detected. Would you like to update to {latest} now?") if should_update: logging.info("Updating mod") install_mod() elif should_update is None: return + + if not args: + should_launch = ask_yes_no_cancel("Launch Game", + "Mod installed and up to date. Would you like to launch the game now?") + if not should_launch: + 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) diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py index 978917c555e1..69dd7aa7f286 100644 --- a/worlds/messenger/connections.py +++ b/worlds/messenger/connections.py @@ -114,7 +114,6 @@ "Forlorn Temple - Rocket Maze Checkpoint", ], "Rocket Maze Checkpoint": [ - "Forlorn Temple - Sunny Day Checkpoint", "Forlorn Temple - Climb Shop", ], }, diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index 8248a4755d3f..a68ee5ba4c7a 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -39,7 +39,9 @@ You can find items wherever items can be picked up in the original game. This in When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint -for it. The groups you can use for The Messenger are: +for it. + +The groups you can use for The Messenger are: * Notes - This covers the music notes * Keys - An alternative name for the music notes * Crest - The Sun and Moon Crests @@ -50,26 +52,26 @@ for it. The groups you can use for The Messenger are: * The player can return to the Tower of Time HQ at any point by selecting the button from the options menu * This can cause issues if used at specific times. If used in any of these known problematic areas, immediately -quit to title and reload the save. The currently known areas include: + quit to title and reload the save. The currently known areas include: * During Boss fights * After Courage Note collection (Corrupted Future chase) * After reaching ninja village a teleport option is added to the menu to reach it quickly * Toggle Windmill Shuriken button is added to option menu once the item is received * The mod option menu will also have a hint item button, as well as a release and collect button that are all placed -when the player fulfills the necessary conditions. + when the player fulfills the necessary conditions. * After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be -used to modify certain settings such as text size and color. This can also be used to specify a player name that can't -be entered in game. + used to modify certain settings such as text size and color. This can also be used to specify a player name that can't + be entered in game. ## Known issues * Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item * If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit -to Searing Crags and re-enter to get it to play correctly. + to Searing Crags and re-enter to get it to play correctly. * Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left -and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock + and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock * Text entry menus don't accept controller input * In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the -chest will not work. + chest will not work. ## What do I do if I have a problem? diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index c1770e747442..64b706c2643a 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -41,14 +41,27 @@ These steps can also be followed to launch the game and check for mod updates af ## Joining a MultiWorld Game +### Automatic Connection on archipelago.gg + +1. Go to the room page of the MultiWorld you are going to join. +2. Click on your slot name on the left side. +3. Click the "The Messenger" button in the prompt. +4. Follow the remaining prompts. This process will check that you have the mod installed and will also check for updates + before launching The Messenger. If you are using the Steam version of The Messenger you may also get a prompt from + Steam asking if the game should be launched with arguments. These arguments are the URI which the mod uses to + connect. +5. Start a new save. You will already be connected in The Messenger and do not need to go through the menus. + +### Manual Connection + 1. Launch the game 2. Navigate to `Options > Archipelago Options` 3. Enter connection info using the relevant option buttons * **The game is limited to alphanumerical characters, `.`, and `-`.** * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the -website. + website. * If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game -directory. When using this, all connection information must be entered in the file. + directory. When using this, all connection information must be entered in the file. 4. Select the `Connect to Archipelago` button 5. Navigate to save file selection 6. Start a new game diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 1da210cb23ff..17152a1a1538 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -215,13 +215,13 @@ def create_mapping(in_portal: str, warp: str) -> str: if "Portal" in warp: exit_string += "Portal" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}00")) elif warp in SHOP_POINTS[parent]: exit_string += f"{warp} Shop" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) else: exit_string += f"{warp} Checkpoint" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) + world.portal_mapping.insert(PORTALS.index(in_portal), int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) world.spoiler_portal_mapping[in_portal] = exit_string connect_portal(world, in_portal, exit_string) @@ -230,12 +230,15 @@ def create_mapping(in_portal: str, warp: str) -> str: def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: """checks the provided plando connections for portals and connects them""" + nonlocal available_portals + for connection in plando_connections: - if connection.entrance not in PORTALS: - continue # let it crash here if input is invalid - create_mapping(connection.entrance, connection.exit) + available_portals.remove(connection.exit) + parent = create_mapping(connection.entrance, connection.exit) world.plando_portals.append(connection.entrance) + if shuffle_type < ShufflePortals.option_anywhere: + available_portals = [port for port in available_portals if port not in shop_points[parent]] shuffle_type = world.options.shuffle_portals shop_points = deepcopy(SHOP_POINTS) @@ -251,8 +254,13 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: plando = world.options.portal_plando.value if not plando: plando = world.options.plando_connections.value - if plando and world.multiworld.plando_options & PlandoOptions.connections: - handle_planned_portals(plando) + if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals: + try: + handle_planned_portals(plando) + # any failure i expect will trigger on available_portals.remove + except ValueError: + raise ValueError(f"Unable to complete portal plando for Player {world.player_name}. " + f"If you attempted to plando a checkpoint, checkpoints must be shuffled.") for portal in PORTALS: if portal in world.plando_portals: @@ -276,8 +284,13 @@ def disconnect_portals(world: "MessengerWorld") -> None: entrance.connected_region = None if portal in world.spoiler_portal_mapping: del world.spoiler_portal_mapping[portal] - if len(world.portal_mapping) > len(world.spoiler_portal_mapping): - world.portal_mapping = world.portal_mapping[:len(world.spoiler_portal_mapping)] + if world.plando_portals: + indexes = [PORTALS.index(portal) for portal in world.plando_portals] + planned_portals = [] + for index, portal_coord in enumerate(world.portal_mapping): + if index in indexes: + planned_portals.append(portal_coord) + world.portal_mapping = planned_portals def validate_portals(world: "MessengerWorld") -> bool: diff --git a/worlds/mlss/Client.py b/worlds/mlss/Client.py index 1f08b85610d6..75f6ac653003 100644 --- a/worlds/mlss/Client.py +++ b/worlds/mlss/Client.py @@ -85,7 +85,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: if not self.seed_verify: seed = await bizhawk.read(ctx.bizhawk_ctx, [(0xDF00A0, len(ctx.seed_name), "ROM")]) seed = seed[0].decode("UTF-8") - if seed != ctx.seed_name: + if seed not in ctx.seed_name: logger.info( "ERROR: The ROM you loaded is for a different game of AP. " "Please make sure the host has sent you the correct patch file," @@ -143,17 +143,30 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # If RAM address isn't 0x0 yet break out and try again later to give the rest of the items for i in range(len(ctx.items_received) - received_index): item_data = items_by_id[ctx.items_received[received_index + i].item] - b = await bizhawk.guarded_read(ctx.bizhawk_ctx, [(0x3057, 1, "EWRAM")], [(0x3057, [0x0], "EWRAM")]) - if b is None: + result = False + total = 0 + while not result: + await asyncio.sleep(0.05) + total += 0.05 + result = await bizhawk.guarded_write( + ctx.bizhawk_ctx, + [ + (0x3057, [id_to_RAM(item_data.itemID)], "EWRAM") + ], + [(0x3057, [0x0], "EWRAM")] + ) + if result: + total = 0 + if total >= 1: + break + if not result: break await bizhawk.write( ctx.bizhawk_ctx, [ - (0x3057, [id_to_RAM(item_data.itemID)], "EWRAM"), (0x4808, [(received_index + i + 1) // 0x100, (received_index + i + 1) % 0x100], "EWRAM"), - ], + ] ) - await asyncio.sleep(0.1) # Early return and location send if you are currently in a shop, # since other flags aren't going to change diff --git a/worlds/mlss/Data.py b/worlds/mlss/Data.py index 749e63bcf24d..add14aa008f1 100644 --- a/worlds/mlss/Data.py +++ b/worlds/mlss/Data.py @@ -1,6 +1,9 @@ flying = [ 0x14, 0x1D, + 0x32, + 0x33, + 0x40, 0x4C ] @@ -23,7 +26,6 @@ 0x5032AC, 0x5032CC, 0x5032EC, - 0x50330C, 0x50332C, 0x50334C, 0x50336C, @@ -151,7 +153,7 @@ 0x50458C, 0x5045AC, 0x50468C, - 0x5046CC, + # 0x5046CC, 6 enemy formation 0x5046EC, 0x50470C ] diff --git a/worlds/mlss/Items.py b/worlds/mlss/Items.py index b95f1a0bc0a8..717443ddfc06 100644 --- a/worlds/mlss/Items.py +++ b/worlds/mlss/Items.py @@ -78,21 +78,21 @@ class MLSSItem(Item): ItemData(77771060, "Beanstar Piece 3", ItemClassification.progression, 0x67), ItemData(77771061, "Beanstar Piece 4", ItemClassification.progression, 0x70), ItemData(77771062, "Spangle", ItemClassification.progression, 0x72), - ItemData(77771063, "Beanlet 1", ItemClassification.filler, 0x73), - ItemData(77771064, "Beanlet 2", ItemClassification.filler, 0x74), - ItemData(77771065, "Beanlet 3", ItemClassification.filler, 0x75), - ItemData(77771066, "Beanlet 4", ItemClassification.filler, 0x76), - ItemData(77771067, "Beanlet 5", ItemClassification.filler, 0x77), - ItemData(77771068, "Beanstone 1", ItemClassification.filler, 0x80), - ItemData(77771069, "Beanstone 2", ItemClassification.filler, 0x81), - ItemData(77771070, "Beanstone 3", ItemClassification.filler, 0x82), - ItemData(77771071, "Beanstone 4", ItemClassification.filler, 0x83), - ItemData(77771072, "Beanstone 5", ItemClassification.filler, 0x84), - ItemData(77771073, "Beanstone 6", ItemClassification.filler, 0x85), - ItemData(77771074, "Beanstone 7", ItemClassification.filler, 0x86), - ItemData(77771075, "Beanstone 8", ItemClassification.filler, 0x87), - ItemData(77771076, "Beanstone 9", ItemClassification.filler, 0x90), - ItemData(77771077, "Beanstone 10", ItemClassification.filler, 0x91), + ItemData(77771063, "Beanlet 1", ItemClassification.useful, 0x73), + ItemData(77771064, "Beanlet 2", ItemClassification.useful, 0x74), + ItemData(77771065, "Beanlet 3", ItemClassification.useful, 0x75), + ItemData(77771066, "Beanlet 4", ItemClassification.useful, 0x76), + ItemData(77771067, "Beanlet 5", ItemClassification.useful, 0x77), + ItemData(77771068, "Beanstone 1", ItemClassification.useful, 0x80), + ItemData(77771069, "Beanstone 2", ItemClassification.useful, 0x81), + ItemData(77771070, "Beanstone 3", ItemClassification.useful, 0x82), + ItemData(77771071, "Beanstone 4", ItemClassification.useful, 0x83), + ItemData(77771072, "Beanstone 5", ItemClassification.useful, 0x84), + ItemData(77771073, "Beanstone 6", ItemClassification.useful, 0x85), + ItemData(77771074, "Beanstone 7", ItemClassification.useful, 0x86), + ItemData(77771075, "Beanstone 8", ItemClassification.useful, 0x87), + ItemData(77771076, "Beanstone 9", ItemClassification.useful, 0x90), + ItemData(77771077, "Beanstone 10", ItemClassification.useful, 0x91), ItemData(77771078, "Secret Scroll 1", ItemClassification.useful, 0x92), ItemData(77771079, "Secret Scroll 2", ItemClassification.useful, 0x93), ItemData(77771080, "Castle Badge", ItemClassification.useful, 0x9F), diff --git a/worlds/mlss/Locations.py b/worlds/mlss/Locations.py index 8c00432a8f06..a2787ef9b1b1 100644 --- a/worlds/mlss/Locations.py +++ b/worlds/mlss/Locations.py @@ -4,9 +4,6 @@ class LocationData: - name: str = "" - id: int = 0x00 - def __init__(self, name, id_, itemType): self.name = name self.itemType = itemType @@ -93,8 +90,8 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Below Summit Block 1", 0x39D873, 0), LocationData("Hoohoo Mountain Below Summit Block 2", 0x39D87B, 0), LocationData("Hoohoo Mountain Below Summit Block 3", 0x39D883, 0), - LocationData("Hoohoo Mountain After Hoohooros Block 1", 0x39D890, 0), - LocationData("Hoohoo Mountain After Hoohooros Block 2", 0x39D8A0, 0), + LocationData("Hoohoo Mountain Past Hoohooros Block 1", 0x39D890, 0), + LocationData("Hoohoo Mountain Past Hoohooros Block 2", 0x39D8A0, 0), LocationData("Hoohoo Mountain Hoohooros Room Block 1", 0x39D8AD, 0), LocationData("Hoohoo Mountain Hoohooros Room Block 2", 0x39D8B5, 0), LocationData("Hoohoo Mountain Before Hoohooros Block", 0x39D8D2, 0), @@ -104,7 +101,7 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Room 1 Block 2", 0x39D924, 0), LocationData("Hoohoo Mountain Room 1 Block 3", 0x39D92C, 0), LocationData("Hoohoo Mountain Base Room 1 Block", 0x39D939, 0), - LocationData("Hoohoo Village Right Side Block", 0x39D957, 0), + LocationData("Hoohoo Village Eastside Block", 0x39D957, 0), LocationData("Hoohoo Village Bridge Room Block 1", 0x39D96F, 0), LocationData("Hoohoo Village Bridge Room Block 2", 0x39D97F, 0), LocationData("Hoohoo Village Bridge Room Block 3", 0x39D98F, 0), @@ -119,8 +116,8 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Base Boostatue Room Digspot 2", 0x39D9E1, 0), LocationData("Hoohoo Mountain Base Grassy Area Block 1", 0x39D9FE, 0), LocationData("Hoohoo Mountain Base Grassy Area Block 2", 0x39D9F6, 0), - LocationData("Hoohoo Mountain Base After Minecart Minigame Block 1", 0x39DA35, 0), - LocationData("Hoohoo Mountain Base After Minecart Minigame Block 2", 0x39DA2D, 0), + LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 1", 0x39DA35, 0), + LocationData("Hoohoo Mountain Base Past Minecart Minigame Block 2", 0x39DA2D, 0), LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 1", 0x39DA77, 0), LocationData("Cave Connecting Stardust Fields and Hoohoo Village Block 2", 0x39DA7F, 0), LocationData("Hoohoo Village South Cave Block", 0x39DACD, 0), @@ -143,14 +140,14 @@ class MLSSLocation(Location): LocationData("Shop Starting Flag 3", 0x3C05F4, 3), LocationData("Hoohoo Mountain Summit Digspot", 0x39D85E, 0), LocationData("Hoohoo Mountain Below Summit Digspot", 0x39D86B, 0), - LocationData("Hoohoo Mountain After Hoohooros Digspot", 0x39D898, 0), + LocationData("Hoohoo Mountain Past Hoohooros Digspot", 0x39D898, 0), LocationData("Hoohoo Mountain Hoohooros Room Digspot 1", 0x39D8BD, 0), LocationData("Hoohoo Mountain Hoohooros Room Digspot 2", 0x39D8C5, 0), LocationData("Hoohoo Mountain Before Hoohooros Digspot", 0x39D8E2, 0), LocationData("Hoohoo Mountain Room 2 Digspot 1", 0x39D907, 0), LocationData("Hoohoo Mountain Room 2 Digspot 2", 0x39D90F, 0), LocationData("Hoohoo Mountain Base Room 1 Digspot", 0x39D941, 0), - LocationData("Hoohoo Village Right Side Digspot", 0x39D95F, 0), + LocationData("Hoohoo Village Eastside Digspot", 0x39D95F, 0), LocationData("Hoohoo Village Super Hammer Cave Digspot", 0x39DB02, 0), LocationData("Hoohoo Village Super Hammer Cave Block", 0x39DAEA, 0), LocationData("Hoohoo Village North Cave Room 2 Digspot", 0x39DAB5, 0), @@ -267,7 +264,7 @@ class MLSSLocation(Location): LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0), LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0), LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0), - LocationData("Chucklehuck Woods After Chuckleroot Coin Block", 0x39DF14, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Coin Block", 0x39DF14, 0), LocationData("Chucklehuck Woods Koopa Room Coin Block", 0x39DF53, 0), LocationData("Chucklehuck Woods Winkle Area Cave Coin Block", 0x39DF80, 0), LocationData("Sewers Prison Room Coin Block", 0x39E01E, 0), @@ -286,11 +283,12 @@ class MLSSLocation(Location): LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1", 0x39DA42, 0), LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2", 0x39DA4A, 0), LocationData("Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3", 0x39DA52, 0), - LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Rightside)", 0x39D9E9, 0), + LocationData("Hoohoo Mountain Base Boostatue Room Digspot 3 (Right Side)", 0x39D9E9, 0), LocationData("Hoohoo Mountain Base Mole Near Teehee Valley", 0x277A45, 1), LocationData("Teehee Valley Entrance To Hoohoo Mountain Digspot", 0x39E5B5, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 1", 0x39E5C8, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 2 Digspot 2", 0x39E5D0, 0), + LocationData("Teehee Valley Upper Maze Room 1 Block", 0x39E5E0, 0), + LocationData("Teehee Valley Upper Maze Room 2 Digspot 1", 0x39E5C8, 0), + LocationData("Teehee Valley Upper Maze Room 2 Digspot 2", 0x39E5D0, 0), LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0), LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0), LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0), @@ -345,12 +343,12 @@ class MLSSLocation(Location): LocationData("Chucklehuck Woods Southwest of Chuckleroot Block", 0x39DEC2, 0), LocationData("Chucklehuck Woods Wiggler room Digspot 1", 0x39DECF, 0), LocationData("Chucklehuck Woods Wiggler room Digspot 2", 0x39DED7, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 1", 0x39DEE4, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 2", 0x39DEEC, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 3", 0x39DEF4, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 4", 0x39DEFC, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 5", 0x39DF04, 0), - LocationData("Chucklehuck Woods After Chuckleroot Block 6", 0x39DF0C, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 1", 0x39DEE4, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 2", 0x39DEEC, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 3", 0x39DEF4, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 4", 0x39DEFC, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 5", 0x39DF04, 0), + LocationData("Chucklehuck Woods Past Chuckleroot Block 6", 0x39DF0C, 0), LocationData("Chucklehuck Woods Koopa Room Block 1", 0x39DF4B, 0), LocationData("Chucklehuck Woods Koopa Room Block 2", 0x39DF5B, 0), LocationData("Chucklehuck Woods Koopa Room Digspot", 0x39DF63, 0), @@ -367,14 +365,14 @@ class MLSSLocation(Location): ] castleTown: typing.List[LocationData] = [ - LocationData("Beanbean Castle Town Left Side House Block 1", 0x39D7A4, 0), - LocationData("Beanbean Castle Town Left Side House Block 2", 0x39D7AC, 0), - LocationData("Beanbean Castle Town Left Side House Block 3", 0x39D7B4, 0), - LocationData("Beanbean Castle Town Left Side House Block 4", 0x39D7BC, 0), - LocationData("Beanbean Castle Town Right Side House Block 1", 0x39D7D8, 0), - LocationData("Beanbean Castle Town Right Side House Block 2", 0x39D7E0, 0), - LocationData("Beanbean Castle Town Right Side House Block 3", 0x39D7E8, 0), - LocationData("Beanbean Castle Town Right Side House Block 4", 0x39D7F0, 0), + LocationData("Beanbean Castle Town West Side House Block 1", 0x39D7A4, 0), + LocationData("Beanbean Castle Town West Side House Block 2", 0x39D7AC, 0), + LocationData("Beanbean Castle Town West Side House Block 3", 0x39D7B4, 0), + LocationData("Beanbean Castle Town West Side House Block 4", 0x39D7BC, 0), + LocationData("Beanbean Castle Town East Side House Block 1", 0x39D7D8, 0), + LocationData("Beanbean Castle Town East Side House Block 2", 0x39D7E0, 0), + LocationData("Beanbean Castle Town East Side House Block 3", 0x39D7E8, 0), + LocationData("Beanbean Castle Town East Side House Block 4", 0x39D7F0, 0), LocationData("Beanbean Castle Peach's Extra Dress", 0x1E9433, 2), LocationData("Beanbean Castle Fake Beanstar", 0x1E9432, 2), LocationData("Beanbean Castle Town Beanlet 1", 0x251347, 1), @@ -444,14 +442,14 @@ class MLSSLocation(Location): ] kidnappedFlag: typing.List[LocationData] = [ - LocationData("Badge Shop Enter Fungitown Flag 1", 0x3C0640, 2), - LocationData("Badge Shop Enter Fungitown Flag 2", 0x3C0642, 2), - LocationData("Badge Shop Enter Fungitown Flag 3", 0x3C0644, 2), - LocationData("Pants Shop Enter Fungitown Flag 1", 0x3C0646, 2), - LocationData("Pants Shop Enter Fungitown Flag 2", 0x3C0648, 2), - LocationData("Pants Shop Enter Fungitown Flag 3", 0x3C064A, 2), - LocationData("Shop Enter Fungitown Flag 1", 0x3C0606, 3), - LocationData("Shop Enter Fungitown Flag 2", 0x3C0608, 3), + LocationData("Badge Shop Trunkle Flag 1", 0x3C0640, 2), + LocationData("Badge Shop Trunkle Flag 2", 0x3C0642, 2), + LocationData("Badge Shop Trunkle Flag 3", 0x3C0644, 2), + LocationData("Pants Shop Trunkle Flag 1", 0x3C0646, 2), + LocationData("Pants Shop Trunkle Flag 2", 0x3C0648, 2), + LocationData("Pants Shop Trunkle Flag 3", 0x3C064A, 2), + LocationData("Shop Trunkle Flag 1", 0x3C0606, 3), + LocationData("Shop Trunkle Flag 2", 0x3C0608, 3), ] beanstarFlag: typing.List[LocationData] = [ @@ -553,21 +551,21 @@ class MLSSLocation(Location): airport: typing.List[LocationData] = [ LocationData("Airport Entrance Digspot", 0x39E2DC, 0), LocationData("Airport Lobby Digspot", 0x39E2E9, 0), - LocationData("Airport Leftside Digspot 1", 0x39E2F6, 0), - LocationData("Airport Leftside Digspot 2", 0x39E2FE, 0), - LocationData("Airport Leftside Digspot 3", 0x39E306, 0), - LocationData("Airport Leftside Digspot 4", 0x39E30E, 0), - LocationData("Airport Leftside Digspot 5", 0x39E316, 0), + LocationData("Airport Westside Digspot 1", 0x39E2F6, 0), + LocationData("Airport Westside Digspot 2", 0x39E2FE, 0), + LocationData("Airport Westside Digspot 3", 0x39E306, 0), + LocationData("Airport Westside Digspot 4", 0x39E30E, 0), + LocationData("Airport Westside Digspot 5", 0x39E316, 0), LocationData("Airport Center Digspot 1", 0x39E323, 0), LocationData("Airport Center Digspot 2", 0x39E32B, 0), LocationData("Airport Center Digspot 3", 0x39E333, 0), LocationData("Airport Center Digspot 4", 0x39E33B, 0), LocationData("Airport Center Digspot 5", 0x39E343, 0), - LocationData("Airport Rightside Digspot 1", 0x39E350, 0), - LocationData("Airport Rightside Digspot 2", 0x39E358, 0), - LocationData("Airport Rightside Digspot 3", 0x39E360, 0), - LocationData("Airport Rightside Digspot 4", 0x39E368, 0), - LocationData("Airport Rightside Digspot 5", 0x39E370, 0), + LocationData("Airport Eastside Digspot 1", 0x39E350, 0), + LocationData("Airport Eastside Digspot 2", 0x39E358, 0), + LocationData("Airport Eastside Digspot 3", 0x39E360, 0), + LocationData("Airport Eastside Digspot 4", 0x39E368, 0), + LocationData("Airport Eastside Digspot 5", 0x39E370, 0), ] gwarharEntrance: typing.List[LocationData] = [ @@ -617,7 +615,6 @@ class MLSSLocation(Location): LocationData("Teehee Valley Past Ultra Hammer Rock Block 2", 0x39E590, 0), LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 1", 0x39E598, 0), LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 3", 0x39E5A8, 0), - LocationData("Teehee Valley Solo Luigi Maze Room 1 Block", 0x39E5E0, 0), LocationData("Teehee Valley Before Trunkle Digspot", 0x39E5F0, 0), LocationData("S.S. Chuckola Storage Room Block 1", 0x39E610, 0), LocationData("S.S. Chuckola Storage Room Block 2", 0x39E628, 0), @@ -667,7 +664,7 @@ class MLSSLocation(Location): LocationData("Bowser's Castle Iggy & Morton Hallway Block 1", 0x39E9EF, 0), LocationData("Bowser's Castle Iggy & Morton Hallway Block 2", 0x39E9F7, 0), LocationData("Bowser's Castle Iggy & Morton Hallway Digspot", 0x39E9FF, 0), - LocationData("Bowser's Castle After Morton Block", 0x39EA0C, 0), + LocationData("Bowser's Castle Past Morton Block", 0x39EA0C, 0), LocationData("Bowser's Castle Morton Room 1 Digspot", 0x39EA89, 0), LocationData("Bowser's Castle Lemmy Room 1 Block", 0x39EA9C, 0), LocationData("Bowser's Castle Lemmy Room 1 Digspot", 0x39EAA4, 0), @@ -705,16 +702,16 @@ class MLSSLocation(Location): LocationData("Joke's End Second Floor West Room Block 4", 0x39E781, 0), LocationData("Joke's End Mole Reward 1", 0x27788E, 1), LocationData("Joke's End Mole Reward 2", 0x2778D2, 1), -] - -jokesMain: typing.List[LocationData] = [ LocationData("Joke's End Furnace Room 1 Block 1", 0x39E70F, 0), LocationData("Joke's End Furnace Room 1 Block 2", 0x39E717, 0), LocationData("Joke's End Furnace Room 1 Block 3", 0x39E71F, 0), LocationData("Joke's End Northeast of Boiler Room 1 Block", 0x39E732, 0), - LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0), LocationData("Joke's End Northeast of Boiler Room 2 Block", 0x39E74C, 0), LocationData("Joke's End Northeast of Boiler Room 2 Digspot", 0x39E754, 0), + LocationData("Joke's End Northeast of Boiler Room 3 Digspot", 0x39E73F, 0), +] + +jokesMain: typing.List[LocationData] = [ LocationData("Joke's End Second Floor East Room Digspot", 0x39E794, 0), LocationData("Joke's End Final Split up Room Digspot", 0x39E7A7, 0), LocationData("Joke's End South of Bridge Room Block", 0x39E7B4, 0), @@ -740,10 +737,10 @@ class MLSSLocation(Location): postJokes: typing.List[LocationData] = [ LocationData("Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)", 0x39E5A0, 0), - LocationData("Teehee Valley Before Popple Digspot 1", 0x39E55B, 0), - LocationData("Teehee Valley Before Popple Digspot 2", 0x39E563, 0), - LocationData("Teehee Valley Before Popple Digspot 3", 0x39E56B, 0), - LocationData("Teehee Valley Before Popple Digspot 4", 0x39E573, 0), + LocationData("Teehee Valley Before Birdo Digspot 1", 0x39E55B, 0), + LocationData("Teehee Valley Before Birdo Digspot 2", 0x39E563, 0), + LocationData("Teehee Valley Before Birdo Digspot 3", 0x39E56B, 0), + LocationData("Teehee Valley Before Birdo Digspot 4", 0x39E573, 0), ] theater: typing.List[LocationData] = [ @@ -766,6 +763,10 @@ class MLSSLocation(Location): LocationData("Oho Oasis Thunderhand", 0x1E9409, 2), ] +cacklettas_soul: typing.List[LocationData] = [ + LocationData("Cackletta's Soul", None, 0), +] + nonBlock = [ (0x434B, 0x1, 0x243844), # Farm Mole 1 (0x434B, 0x1, 0x24387D), # Farm Mole 2 @@ -1171,15 +1172,15 @@ class MLSSLocation(Location): + fungitownBeanstar + fungitownBirdo + bowsers + + bowsersMini + jokesEntrance + jokesMain + postJokes + theater + oasis + gwarharMain - + bowsersMini + baseUltraRocks + coins ) -location_table: typing.Dict[str, int] = {locData.name: locData.id for locData in all_locations} +location_table: typing.Dict[str, int] = {location.name: location.id for location in all_locations} diff --git a/worlds/mlss/Names/LocationName.py b/worlds/mlss/Names/LocationName.py index 7cbc2e4f31f8..5b38b2a10f6e 100644 --- a/worlds/mlss/Names/LocationName.py +++ b/worlds/mlss/Names/LocationName.py @@ -8,14 +8,14 @@ class LocationName: StardustFields4Block3 = "Stardust Fields Room 4 Block 3" StardustFields5Block = "Stardust Fields Room 5 Block" HoohooVillageHammerHouseBlock = "Hoohoo Village Hammer House Block" - BeanbeanCastleTownLeftSideHouseBlock1 = "Beanbean Castle Town Left Side House Block 1" - BeanbeanCastleTownLeftSideHouseBlock2 = "Beanbean Castle Town Left Side House Block 2" - BeanbeanCastleTownLeftSideHouseBlock3 = "Beanbean Castle Town Left Side House Block 3" - BeanbeanCastleTownLeftSideHouseBlock4 = "Beanbean Castle Town Left Side House Block 4" - BeanbeanCastleTownRightSideHouseBlock1 = "Beanbean Castle Town Right Side House Block 1" - BeanbeanCastleTownRightSideHouseBlock2 = "Beanbean Castle Town Right Side House Block 2" - BeanbeanCastleTownRightSideHouseBlock3 = "Beanbean Castle Town Right Side House Block 3" - BeanbeanCastleTownRightSideHouseBlock4 = "Beanbean Castle Town Right Side House Block 4" + BeanbeanCastleTownWestsideHouseBlock1 = "Beanbean Castle Town Westside House Block 1" + BeanbeanCastleTownWestsideHouseBlock2 = "Beanbean Castle Town Westside House Block 2" + BeanbeanCastleTownWestsideHouseBlock3 = "Beanbean Castle Town Westside House Block 3" + BeanbeanCastleTownWestsideHouseBlock4 = "Beanbean Castle Town Westside House Block 4" + BeanbeanCastleTownEastsideHouseBlock1 = "Beanbean Castle Town Eastside House Block 1" + BeanbeanCastleTownEastsideHouseBlock2 = "Beanbean Castle Town Eastside House Block 2" + BeanbeanCastleTownEastsideHouseBlock3 = "Beanbean Castle Town Eastside House Block 3" + BeanbeanCastleTownEastsideHouseBlock4 = "Beanbean Castle Town Eastside House Block 4" BeanbeanCastleTownMiniMarioBlock1 = "Beanbean Castle Town Mini Mario Block 1" BeanbeanCastleTownMiniMarioBlock2 = "Beanbean Castle Town Mini Mario Block 2" BeanbeanCastleTownMiniMarioBlock3 = "Beanbean Castle Town Mini Mario Block 3" @@ -26,9 +26,9 @@ class LocationName: HoohooMountainBelowSummitBlock1 = "Hoohoo Mountain Below Summit Block 1" HoohooMountainBelowSummitBlock2 = "Hoohoo Mountain Below Summit Block 2" HoohooMountainBelowSummitBlock3 = "Hoohoo Mountain Below Summit Block 3" - HoohooMountainAfterHoohoorosBlock1 = "Hoohoo Mountain After Hoohooros Block 1" - HoohooMountainAfterHoohoorosDigspot = "Hoohoo Mountain After Hoohooros Digspot" - HoohooMountainAfterHoohoorosBlock2 = "Hoohoo Mountain After Hoohooros Block 2" + HoohooMountainPastHoohoorosBlock1 = "Hoohoo Mountain Past Hoohooros Block 1" + HoohooMountainPastHoohoorosDigspot = "Hoohoo Mountain Past Hoohooros Digspot" + HoohooMountainPastHoohoorosBlock2 = "Hoohoo Mountain Past Hoohooros Block 2" HoohooMountainHoohoorosRoomBlock1 = "Hoohoo Mountain Hoohooros Room Block 1" HoohooMountainHoohoorosRoomBlock2 = "Hoohoo Mountain Hoohooros Room Block 2" HoohooMountainHoohoorosRoomDigspot1 = "Hoohoo Mountain Hoohooros Room Digspot 1" @@ -44,8 +44,8 @@ class LocationName: HoohooMountainRoom1Block3 = "Hoohoo Mountain Room 1 Block 3" HoohooMountainBaseRoom1Block = "Hoohoo Mountain Base Room 1 Block" HoohooMountainBaseRoom1Digspot = "Hoohoo Mountain Base Room 1 Digspot" - HoohooVillageRightSideBlock = "Hoohoo Village Right Side Block" - HoohooVillageRightSideDigspot = "Hoohoo Village Right Side Digspot" + HoohooVillageEastsideBlock = "Hoohoo Village Eastside Block" + HoohooVillageEastsideDigspot = "Hoohoo Village Eastside Digspot" HoohooVillageBridgeRoomBlock1 = "Hoohoo Village Bridge Room Block 1" HoohooVillageBridgeRoomBlock2 = "Hoohoo Village Bridge Room Block 2" HoohooVillageBridgeRoomBlock3 = "Hoohoo Village Bridge Room Block 3" @@ -65,8 +65,8 @@ class LocationName: HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot" HoohooMountainBaseTeeheeValleyEntranceDigspot = "Hoohoo Mountain Base Teehee Valley Entrance Digspot" HoohooMountainBaseTeeheeValleyEntranceBlock = "Hoohoo Mountain Base Teehee Valley Entrance Block" - HoohooMountainBaseAfterMinecartMinigameBlock1 = "Hoohoo Mountain Base After Minecart Minigame Block 1" - HoohooMountainBaseAfterMinecartMinigameBlock2 = "Hoohoo Mountain Base After Minecart Minigame Block 2" + HoohooMountainBasePastMinecartMinigameBlock1 = "Hoohoo Mountain Base Past Minecart Minigame Block 1" + HoohooMountainBasePastMinecartMinigameBlock2 = "Hoohoo Mountain Base Past Minecart Minigame Block 2" HoohooMountainBasePastUltraHammerRocksBlock1 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 1" HoohooMountainBasePastUltraHammerRocksBlock2 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 2" HoohooMountainBasePastUltraHammerRocksBlock3 = "Hoohoo Mountain Base Past Ultra Hammer Rocks Block 3" @@ -148,12 +148,12 @@ class LocationName: ChucklehuckWoodsSouthwestOfChucklerootBlock = "Chucklehuck Woods Southwest of Chuckleroot Block" ChucklehuckWoodsWigglerRoomDigspot1 = "Chucklehuck Woods Wiggler Room Digspot 1" ChucklehuckWoodsWigglerRoomDigspot2 = "Chucklehuck Woods Wiggler Room Digspot 2" - ChucklehuckWoodsAfterChucklerootBlock1 = "Chucklehuck Woods After Chuckleroot Block 1" - ChucklehuckWoodsAfterChucklerootBlock2 = "Chucklehuck Woods After Chuckleroot Block 2" - ChucklehuckWoodsAfterChucklerootBlock3 = "Chucklehuck Woods After Chuckleroot Block 3" - ChucklehuckWoodsAfterChucklerootBlock4 = "Chucklehuck Woods After Chuckleroot Block 4" - ChucklehuckWoodsAfterChucklerootBlock5 = "Chucklehuck Woods After Chuckleroot Block 5" - ChucklehuckWoodsAfterChucklerootBlock6 = "Chucklehuck Woods After Chuckleroot Block 6" + ChucklehuckWoodsPastChucklerootBlock1 = "Chucklehuck Woods Past Chuckleroot Block 1" + ChucklehuckWoodsPastChucklerootBlock2 = "Chucklehuck Woods Past Chuckleroot Block 2" + ChucklehuckWoodsPastChucklerootBlock3 = "Chucklehuck Woods Past Chuckleroot Block 3" + ChucklehuckWoodsPastChucklerootBlock4 = "Chucklehuck Woods Past Chuckleroot Block 4" + ChucklehuckWoodsPastChucklerootBlock5 = "Chucklehuck Woods Past Chuckleroot Block 5" + ChucklehuckWoodsPastChucklerootBlock6 = "Chucklehuck Woods Past Chuckleroot Block 6" WinkleAreaBeanstarRoomBlock = "Winkle Area Beanstar Room Block" WinkleAreaDigspot = "Winkle Area Digspot" WinkleAreaOutsideColosseumBlock = "Winkle Area Outside Colosseum Block" @@ -232,21 +232,21 @@ class LocationName: WoohooHooniversityPastCacklettaRoom2Digspot = "Woohoo Hooniversity Past Cackletta Room 2 Digspot" AirportEntranceDigspot = "Airport Entrance Digspot" AirportLobbyDigspot = "Airport Lobby Digspot" - AirportLeftsideDigspot1 = "Airport Leftside Digspot 1" - AirportLeftsideDigspot2 = "Airport Leftside Digspot 2" - AirportLeftsideDigspot3 = "Airport Leftside Digspot 3" - AirportLeftsideDigspot4 = "Airport Leftside Digspot 4" - AirportLeftsideDigspot5 = "Airport Leftside Digspot 5" + AirportWestsideDigspot1 = "Airport Westside Digspot 1" + AirportWestsideDigspot2 = "Airport Westside Digspot 2" + AirportWestsideDigspot3 = "Airport Westside Digspot 3" + AirportWestsideDigspot4 = "Airport Westside Digspot 4" + AirportWestsideDigspot5 = "Airport Westside Digspot 5" AirportCenterDigspot1 = "Airport Center Digspot 1" AirportCenterDigspot2 = "Airport Center Digspot 2" AirportCenterDigspot3 = "Airport Center Digspot 3" AirportCenterDigspot4 = "Airport Center Digspot 4" AirportCenterDigspot5 = "Airport Center Digspot 5" - AirportRightsideDigspot1 = "Airport Rightside Digspot 1" - AirportRightsideDigspot2 = "Airport Rightside Digspot 2" - AirportRightsideDigspot3 = "Airport Rightside Digspot 3" - AirportRightsideDigspot4 = "Airport Rightside Digspot 4" - AirportRightsideDigspot5 = "Airport Rightside Digspot 5" + AirportEastsideDigspot1 = "Airport Eastside Digspot 1" + AirportEastsideDigspot2 = "Airport Eastside Digspot 2" + AirportEastsideDigspot3 = "Airport Eastside Digspot 3" + AirportEastsideDigspot4 = "Airport Eastside Digspot 4" + AirportEastsideDigspot5 = "Airport Eastside Digspot 5" GwarharLagoonPipeRoomDigspot = "Gwarhar Lagoon Pipe Room Digspot" GwarharLagoonMassageParlorEntranceDigspot = "Gwarhar Lagoon Massage Parlor Entrance Digspot" GwarharLagoonPastHermieDigspot = "Gwarhar Lagoon Past Hermie Digspot" @@ -276,10 +276,10 @@ class LocationName: WoohooHooniversityBasementRoom4Block = "Woohoo Hooniversity Basement Room 4 Block" WoohooHooniversityPoppleRoomDigspot1 = "Woohoo Hooniversity Popple Room Digspot 1" WoohooHooniversityPoppleRoomDigspot2 = "Woohoo Hooniversity Popple Room Digspot 2" - TeeheeValleyBeforePoppleDigspot1 = "Teehee Valley Before Popple Digspot 1" - TeeheeValleyBeforePoppleDigspot2 = "Teehee Valley Before Popple Digspot 2" - TeeheeValleyBeforePoppleDigspot3 = "Teehee Valley Before Popple Digspot 3" - TeeheeValleyBeforePoppleDigspot4 = "Teehee Valley Before Popple Digspot 4" + TeeheeValleyBeforeBirdoDigspot1 = "Teehee Valley Before Birdo Digspot 1" + TeeheeValleyBeforeBirdoDigspot2 = "Teehee Valley Before Birdo Digspot 2" + TeeheeValleyBeforeBirdoDigspot3 = "Teehee Valley Before Birdo Digspot 3" + TeeheeValleyBeforeBirdoDigspot4 = "Teehee Valley Before Birdo Digspot 4" TeeheeValleyRoom1Digspot1 = "Teehee Valley Room 1 Digspot 1" TeeheeValleyRoom1Digspot2 = "Teehee Valley Room 1 Digspot 2" TeeheeValleyRoom1Digspot3 = "Teehee Valley Room 1 Digspot 3" @@ -296,9 +296,9 @@ class LocationName: TeeheeValleyPastUltraHammersDigspot2 = "Teehee Valley Past Ultra Hammer Rock Digspot 2 (Post-Birdo)" TeeheeValleyPastUltraHammersDigspot3 = "Teehee Valley Past Ultra Hammer Rock Digspot 3" TeeheeValleyEntranceToHoohooMountainDigspot = "Teehee Valley Entrance To Hoohoo Mountain Digspot" - TeeheeValleySoloLuigiMazeRoom2Digspot1 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 1" - TeeheeValleySoloLuigiMazeRoom2Digspot2 = "Teehee Valley Solo Luigi Maze Room 2 Digspot 2" - TeeheeValleySoloLuigiMazeRoom1Block = "Teehee Valley Solo Luigi Maze Room 1 Block" + TeeheeValleyUpperMazeRoom2Digspot1 = "Teehee Valley Upper Maze Room 2 Digspot 1" + TeeheeValleyUpperMazeRoom2Digspot2 = "Teehee Valley Upper Maze Room 2 Digspot 2" + TeeheeValleyUpperMazeRoom1Block = "Teehee Valley Upper Maze Room 1 Block" TeeheeValleyBeforeTrunkleDigspot = "Teehee Valley Before Trunkle Digspot" TeeheeValleyTrunkleRoomDigspot = "Teehee Valley Trunkle Room Digspot" SSChuckolaStorageRoomBlock1 = "S.S. Chuckola Storage Room Block 1" @@ -314,10 +314,10 @@ class LocationName: JokesEndFurnaceRoom1Block1 = "Joke's End Furnace Room 1 Block 1" JokesEndFurnaceRoom1Block2 = "Joke's End Furnace Room 1 Block 2" JokesEndFurnaceRoom1Block3 = "Joke's End Furnace Room 1 Block 3" - JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast Of Boiler Room 1 Block" - JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast Of Boiler Room 3 Digspot" - JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast Of Boiler Room 2 Block" - JokesEndNortheastOfBoilerRoom2Block2 = "Joke's End Northeast Of Boiler Room 2 Digspot" + JokesEndNortheastOfBoilerRoom1Block = "Joke's End Northeast of Boiler Room 1 Block" + JokesEndNortheastOfBoilerRoom3Digspot = "Joke's End Northeast of Boiler Room 3 Digspot" + JokesEndNortheastOfBoilerRoom2Block1 = "Joke's End Northeast of Boiler Room 2 Block" + JokesEndNortheastOfBoilerRoom2Digspot = "Joke's End Northeast of Boiler Room 2 Digspot" JokesEndSecondFloorWestRoomBlock1 = "Joke's End Second Floor West Room Block 1" JokesEndSecondFloorWestRoomBlock2 = "Joke's End Second Floor West Room Block 2" JokesEndSecondFloorWestRoomBlock3 = "Joke's End Second Floor West Room Block 3" @@ -505,7 +505,7 @@ class LocationName: BowsersCastleIggyMortonHallwayBlock1 = "Bowser's Castle Iggy & Morton Hallway Block 1" BowsersCastleIggyMortonHallwayBlock2 = "Bowser's Castle Iggy & Morton Hallway Block 2" BowsersCastleIggyMortonHallwayDigspot = "Bowser's Castle Iggy & Morton Hallway Digspot" - BowsersCastleAfterMortonBlock = "Bowser's Castle After Morton Block" + BowsersCastlePastMortonBlock = "Bowser's Castle Past Morton Block" BowsersCastleLudwigRoyHallwayBlock1 = "Bowser's Castle Ludwig & Roy Hallway Block 1" BowsersCastleLudwigRoyHallwayBlock2 = "Bowser's Castle Ludwig & Roy Hallway Block 2" BowsersCastleRoyCorridorBlock1 = "Bowser's Castle Roy Corridor Block 1" @@ -546,7 +546,7 @@ class LocationName: ChucklehuckWoodsCaveRoom3CoinBlock = "Chucklehuck Woods Cave Room 3 Coin Block" ChucklehuckWoodsPipe5RoomCoinBlock = "Chucklehuck Woods Pipe 5 Room Coin Block" ChucklehuckWoodsRoom7CoinBlock = "Chucklehuck Woods Room 7 Coin Block" - ChucklehuckWoodsAfterChucklerootCoinBlock = "Chucklehuck Woods After Chuckleroot Coin Block" + ChucklehuckWoodsPastChucklerootCoinBlock = "Chucklehuck Woods Past Chuckleroot Coin Block" ChucklehuckWoodsKoopaRoomCoinBlock = "Chucklehuck Woods Koopa Room Coin Block" ChucklehuckWoodsWinkleAreaCaveCoinBlock = "Chucklehuck Woods Winkle Area Cave Coin Block" SewersPrisonRoomCoinBlock = "Sewers Prison Room Coin Block" diff --git a/worlds/mlss/Options.py b/worlds/mlss/Options.py index 14c1ef3a7d5a..73e8ebd4015f 100644 --- a/worlds/mlss/Options.py +++ b/worlds/mlss/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range +from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Range, Removed from dataclasses import dataclass @@ -282,7 +282,8 @@ class MLSSOptions(PerGameCommonOptions): extra_pipes: ExtraPipes skip_minecart: SkipMinecart disable_surf: DisableSurf - harhalls_pants: HarhallsPants + disable_harhalls_pants: HarhallsPants + harhalls_pants: Removed block_visibility: HiddenVisible chuckle_beans: ChuckleBeans music_options: MusicOptions diff --git a/worlds/mlss/Regions.py b/worlds/mlss/Regions.py index 992e99e2c7f7..7dd5e9451141 100644 --- a/worlds/mlss/Regions.py +++ b/worlds/mlss/Regions.py @@ -33,6 +33,7 @@ postJokes, baseUltraRocks, coins, + cacklettas_soul, ) from . import StateLogic @@ -40,44 +41,45 @@ from . import MLSSWorld -def create_regions(world: "MLSSWorld", excluded: typing.List[str]): +def create_regions(world: "MLSSWorld"): menu_region = Region("Menu", world.player, world.multiworld) world.multiworld.regions.append(menu_region) - create_region(world, "Main Area", mainArea, excluded) - create_region(world, "Chucklehuck Woods", chucklehuck, excluded) - create_region(world, "Beanbean Castle Town", castleTown, excluded) - create_region(world, "Shop Starting Flag", startingFlag, excluded) - create_region(world, "Shop Chuckolator Flag", chuckolatorFlag, excluded) - create_region(world, "Shop Mom Piranha Flag", piranhaFlag, excluded) - create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag, excluded) - create_region(world, "Shop Beanstar Complete Flag", beanstarFlag, excluded) - create_region(world, "Shop Birdo Flag", birdoFlag, excluded) - create_region(world, "Surfable", surfable, excluded) - create_region(world, "Hooniversity", hooniversity, excluded) - create_region(world, "GwarharEntrance", gwarharEntrance, excluded) - create_region(world, "GwarharMain", gwarharMain, excluded) - create_region(world, "TeeheeValley", teeheeValley, excluded) - create_region(world, "Winkle", winkle, excluded) - create_region(world, "Sewers", sewers, excluded) - create_region(world, "Airport", airport, excluded) - create_region(world, "JokesEntrance", jokesEntrance, excluded) - create_region(world, "JokesMain", jokesMain, excluded) - create_region(world, "PostJokes", postJokes, excluded) - create_region(world, "Theater", theater, excluded) - create_region(world, "Fungitown", fungitown, excluded) - create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar, excluded) - create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo, excluded) - create_region(world, "BooStatue", booStatue, excluded) - create_region(world, "Oasis", oasis, excluded) - create_region(world, "BaseUltraRocks", baseUltraRocks, excluded) + create_region(world, "Main Area", mainArea) + create_region(world, "Chucklehuck Woods", chucklehuck) + create_region(world, "Beanbean Castle Town", castleTown) + create_region(world, "Shop Starting Flag", startingFlag) + create_region(world, "Shop Chuckolator Flag", chuckolatorFlag) + create_region(world, "Shop Mom Piranha Flag", piranhaFlag) + create_region(world, "Shop Enter Fungitown Flag", kidnappedFlag) + create_region(world, "Shop Beanstar Complete Flag", beanstarFlag) + create_region(world, "Shop Birdo Flag", birdoFlag) + create_region(world, "Surfable", surfable) + create_region(world, "Hooniversity", hooniversity) + create_region(world, "GwarharEntrance", gwarharEntrance) + create_region(world, "GwarharMain", gwarharMain) + create_region(world, "TeeheeValley", teeheeValley) + create_region(world, "Winkle", winkle) + create_region(world, "Sewers", sewers) + create_region(world, "Airport", airport) + create_region(world, "JokesEntrance", jokesEntrance) + create_region(world, "JokesMain", jokesMain) + create_region(world, "PostJokes", postJokes) + create_region(world, "Theater", theater) + create_region(world, "Fungitown", fungitown) + create_region(world, "Fungitown Shop Beanstar Complete Flag", fungitownBeanstar) + create_region(world, "Fungitown Shop Birdo Flag", fungitownBirdo) + create_region(world, "BooStatue", booStatue) + create_region(world, "Oasis", oasis) + create_region(world, "BaseUltraRocks", baseUltraRocks) + create_region(world, "Cackletta's Soul", cacklettas_soul) if world.options.coins: - create_region(world, "Coins", coins, excluded) + create_region(world, "Coins", coins) if not world.options.castle_skip: - create_region(world, "Bowser's Castle", bowsers, excluded) - create_region(world, "Bowser's Castle Mini", bowsersMini, excluded) + create_region(world, "Bowser's Castle", bowsers) + create_region(world, "Bowser's Castle Mini", bowsersMini) def connect_regions(world: "MLSSWorld"): @@ -221,6 +223,9 @@ def connect_regions(world: "MLSSWorld"): "Bowser's Castle Mini", lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player), ) + connect(world, names, "Bowser's Castle Mini", "Cackletta's Soul") + else: + connect(world, names, "PostJokes", "Cackletta's Soul") connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player)) connect( world, @@ -282,11 +287,11 @@ def connect_regions(world: "MLSSWorld"): ) -def create_region(world: "MLSSWorld", name, locations, excluded): +def create_region(world: "MLSSWorld", name, locations): ret = Region(name, world.player, world.multiworld) for location in locations: loc = MLSSLocation(world.player, location.name, location.id, ret) - if location.name in excluded: + if location.name in world.disabled_locations: continue ret.locations.append(loc) world.multiworld.regions.append(ret) diff --git a/worlds/mlss/Rom.py b/worlds/mlss/Rom.py index 7cbbe8875195..03eac040efb2 100644 --- a/worlds/mlss/Rom.py +++ b/worlds/mlss/Rom.py @@ -8,7 +8,7 @@ from settings import get_settings from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension from .Items import item_table -from .Locations import shop, badge, pants, location_table, hidden, all_locations +from .Locations import shop, badge, pants, location_table, all_locations if TYPE_CHECKING: from . import MLSSWorld @@ -88,7 +88,7 @@ def hidden_visible(caller: APProcedurePatch, rom: bytes): return rom stream = io.BytesIO(rom) - for location in all_locations: + for location in [location for location in all_locations if location.itemType == 0]: stream.seek(location.id - 6) b = stream.read(1) if b[0] == 0x10 and options["block_visibility"] == 1: @@ -133,7 +133,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): stream = io.BytesIO(rom) random.seed(options["seed"] + options["player"]) - if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2) and options["randomize_enemies"] == 0: + if options["randomize_bosses"] == 1 or (options["randomize_bosses"] == 2 and options["randomize_enemies"] == 0): raw = [] for pos in bosses: stream.seek(pos + 1) @@ -164,6 +164,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): enemies_raw = [] groups = [] + boss_groups = [] if options["randomize_enemies"] == 0: return stream.getvalue() @@ -171,7 +172,7 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): if options["randomize_bosses"] == 2: for pos in bosses: stream.seek(pos + 1) - groups += [stream.read(0x1F)] + boss_groups += [stream.read(0x1F)] for pos in enemies: stream.seek(pos + 8) @@ -221,12 +222,19 @@ def enemy_randomize(caller: APProcedurePatch, rom: bytes): groups += [raw] chomp = False - random.shuffle(groups) arr = enemies if options["randomize_bosses"] == 2: arr += bosses + groups += boss_groups + + random.shuffle(groups) for pos in arr: + if arr[-1] in boss_groups: + stream.seek(pos) + temp = stream.read(1) + stream.seek(pos) + stream.write(bytes([temp[0] | 0x8])) stream.seek(pos + 1) stream.write(groups.pop()) @@ -320,20 +328,9 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None: patch.write_token(APTokenTypes.WRITE, address + 3, bytes([world.random.randint(0x0, 0x26)])) for location_name in location_table.keys(): - if ( - (world.options.skip_minecart and "Minecart" in location_name and "After" not in location_name) - or (world.options.castle_skip and "Bowser" in location_name) - or (world.options.disable_surf and "Surf Minigame" in location_name) - or (world.options.harhalls_pants and "Harhall's" in location_name) - ): - continue - if (world.options.chuckle_beans == 0 and "Digspot" in location_name) or ( - world.options.chuckle_beans == 1 and location_table[location_name] in hidden - ): - continue - if not world.options.coins and "Coin" in location_name: + if location_name in world.disabled_locations: continue - location = world.multiworld.get_location(location_name, world.player) + location = world.get_location(location_name) item = location.item address = [address for address in all_locations if address.name == location.name] item_inject(world, patch, location.address, address[0].itemType, item) diff --git a/worlds/mlss/Rules.py b/worlds/mlss/Rules.py index 13627eafc479..b0b5a36465e2 100644 --- a/worlds/mlss/Rules.py +++ b/worlds/mlss/Rules.py @@ -13,7 +13,7 @@ def set_rules(world: "MLSSWorld", excluded): for location in all_locations: if "Digspot" in location.name: if (world.options.skip_minecart and "Minecart" in location.name) or ( - world.options.castle_skip and "Bowser" in location.name + world.options.castle_skip and "Bowser" in location.name ): continue if world.options.chuckle_beans == 0 or world.options.chuckle_beans == 1 and location.id in hidden: @@ -218,9 +218,9 @@ def set_rules(world: "MLSSWorld", excluded): add_rule( world.get_location(LocationName.BeanbeanOutskirtsUltraHammerUpgrade), lambda state: StateLogic.thunder(state, world.player) - and StateLogic.pieces(state, world.player) - and StateLogic.castleTown(state, world.player) - and StateLogic.rose(state, world.player), + and StateLogic.pieces(state, world.player) + and StateLogic.castleTown(state, world.player) + and StateLogic.rose(state, world.player), ) add_rule( world.get_location(LocationName.BeanbeanOutskirtsSoloLuigiCaveMole), @@ -235,27 +235,27 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock1), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock1), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock2), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock2), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock3), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock3), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock4), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock4), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock5), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock5), lambda state: StateLogic.fruits(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootBlock6), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootBlock6), lambda state: StateLogic.fruits(state, world.player), ) add_rule( @@ -350,10 +350,6 @@ def set_rules(world: "MLSSWorld", excluded): world.get_location(LocationName.TeeheeValleyPastUltraHammersBlock2), lambda state: StateLogic.ultra(state, world.player), ) - add_rule( - world.get_location(LocationName.TeeheeValleySoloLuigiMazeRoom1Block), - lambda state: StateLogic.ultra(state, world.player), - ) add_rule( world.get_location(LocationName.OhoOasisFirebrand), lambda state: StateLogic.canMini(state, world.player), @@ -462,6 +458,143 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.canCrash(state, world.player), ) + if world.options.randomize_bosses.value != 0: + if world.options.chuckle_beans != 0: + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainSummitDigspot), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + if world.options.chuckle_beans == 2: + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomDigspot2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomDigspot2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooVillageHammers), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPeasleysRose), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainHoohoorosRoomBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainBelowSummitBlock3), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosBlock1), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosBlock2), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + add_rule( + world.get_location(LocationName.HoohooMountainPastHoohoorosConnectorRoomBlock), + lambda state: StateLogic.hammers(state, world.player) + or StateLogic.fire(state, world.player) + or StateLogic.thunder(state, world.player), + ) + + if not world.options.difficult_logic: + if world.options.chuckle_beans != 0: + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Digspot), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom3Digspot), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom1Block), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndNortheastOfBoilerRoom2Block1), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block1), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block2), + lambda state: StateLogic.canCrash(state, world.player), + ) + add_rule( + world.get_location(LocationName.JokesEndFurnaceRoom1Block3), + lambda state: StateLogic.canCrash(state, world.player), + ) + if world.options.coins: add_rule( world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1), @@ -516,7 +649,7 @@ def set_rules(world: "MLSSWorld", excluded): lambda state: StateLogic.brooch(state, world.player) and StateLogic.hammers(state, world.player), ) add_rule( - world.get_location(LocationName.ChucklehuckWoodsAfterChucklerootCoinBlock), + world.get_location(LocationName.ChucklehuckWoodsPastChucklerootCoinBlock), lambda state: StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player), ) add_rule( @@ -546,23 +679,23 @@ def set_rules(world: "MLSSWorld", excluded): add_rule( world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock), lambda state: StateLogic.canDash(state, world.player) - and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)), + and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)), ) add_rule( world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock), lambda state: StateLogic.ultra(state, world.player) - and StateLogic.fire(state, world.player) - and ( - StateLogic.membership(state, world.player) - or (StateLogic.canDig(state, world.player) and StateLogic.canMini(state, world.player)) - ), + and StateLogic.fire(state, world.player) + and (StateLogic.membership(state, world.player) + or (StateLogic.canDig(state, world.player) + and StateLogic.canMini(state, world.player))), ) add_rule( world.get_location(LocationName.JokesEndNorthofBridgeRoomCoinBlock), lambda state: StateLogic.ultra(state, world.player) - and StateLogic.fire(state, world.player) - and StateLogic.canDig(state, world.player) - and (StateLogic.membership(state, world.player) or StateLogic.canMini(state, world.player)), + and StateLogic.fire(state, world.player) + and StateLogic.canDig(state, world.player) + and (StateLogic.membership(state, world.player) + or StateLogic.canMini(state, world.player)), ) if not world.options.difficult_logic: add_rule( diff --git a/worlds/mlss/__init__.py b/worlds/mlss/__init__.py index f44343c230d0..bb7ed0515419 100644 --- a/worlds/mlss/__init__.py +++ b/worlds/mlss/__init__.py @@ -4,7 +4,7 @@ import settings from BaseClasses import Tutorial, ItemClassification from worlds.AutoWorld import WebWorld, World -from typing import List, Dict, Any +from typing import Set, Dict, Any from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins from .Options import MLSSOptions from .Items import MLSSItem, itemList, item_frequencies, item_table @@ -55,29 +55,29 @@ class MLSSWorld(World): settings: typing.ClassVar[MLSSSettings] item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations} - required_client_version = (0, 4, 5) + required_client_version = (0, 5, 0) - disabled_locations: List[str] + disabled_locations: Set[str] def generate_early(self) -> None: - self.disabled_locations = [] - if self.options.chuckle_beans == 0: - self.disabled_locations += [location.name for location in all_locations if "Digspot" in location.name] - if self.options.castle_skip: - self.disabled_locations += [location.name for location in all_locations if "Bowser" in location.name] - if self.options.chuckle_beans == 1: - self.disabled_locations = [location.name for location in all_locations if location.id in hidden] + self.disabled_locations = set() if self.options.skip_minecart: - self.disabled_locations += [LocationName.HoohooMountainBaseMinecartCaveDigspot] + self.disabled_locations.update([LocationName.HoohooMountainBaseMinecartCaveDigspot]) if self.options.disable_surf: - self.disabled_locations += [LocationName.SurfMinigame] - if self.options.harhalls_pants: - self.disabled_locations += [LocationName.HarhallsPants] + self.disabled_locations.update([LocationName.SurfMinigame]) + if self.options.disable_harhalls_pants: + self.disabled_locations.update([LocationName.HarhallsPants]) + if self.options.chuckle_beans == 0: + self.disabled_locations.update([location.name for location in all_locations if "Digspot" in location.name]) + if self.options.chuckle_beans == 1: + self.disabled_locations.update([location.name for location in all_locations if location.id in hidden]) + if self.options.castle_skip: + self.disabled_locations.update([location.name for location in bowsers + bowsersMini]) if not self.options.coins: - self.disabled_locations += [location.name for location in all_locations if location in coins] + self.disabled_locations.update([location.name for location in coins]) def create_regions(self) -> None: - create_regions(self, self.disabled_locations) + create_regions(self) connect_regions(self) item = self.create_item("Mushroom") @@ -90,13 +90,15 @@ def create_regions(self) -> None: self.get_location(LocationName.PantsShopStartingFlag1).place_locked_item(item) item = self.create_item("Chuckle Bean") self.get_location(LocationName.PantsShopStartingFlag2).place_locked_item(item) + item = MLSSItem("Victory", ItemClassification.progression, None, self.player) + self.get_location("Cackletta's Soul").place_locked_item(item) def fill_slot_data(self) -> Dict[str, Any]: return { "CastleSkip": self.options.castle_skip.value, "SkipMinecart": self.options.skip_minecart.value, "DisableSurf": self.options.disable_surf.value, - "HarhallsPants": self.options.harhalls_pants.value, + "HarhallsPants": self.options.disable_harhalls_pants.value, "ChuckleBeans": self.options.chuckle_beans.value, "DifficultLogic": self.options.difficult_logic.value, "Coins": self.options.coins.value, @@ -111,7 +113,7 @@ def create_items(self) -> None: freq = item_frequencies.get(item.itemName, 1) if item in precollected: freq = max(freq - precollected.count(item), 0) - if self.options.harhalls_pants and "Harhall's" in item.itemName: + if self.options.disable_harhalls_pants and "Harhall's" in item.itemName: continue required_items += [item.itemName for _ in range(freq)] @@ -135,21 +137,7 @@ def create_items(self) -> None: filler_items += [item.itemName for _ in range(freq)] # And finally take as many fillers as we need to have the same amount of items and locations. - remaining = len(all_locations) - len(required_items) - 5 - if self.options.castle_skip: - remaining -= len(bowsers) + len(bowsersMini) - (5 if self.options.chuckle_beans == 0 else 0) - if self.options.skip_minecart and self.options.chuckle_beans == 2: - remaining -= 1 - if self.options.disable_surf: - remaining -= 1 - if self.options.harhalls_pants: - remaining -= 1 - if self.options.chuckle_beans == 0: - remaining -= 192 - if self.options.chuckle_beans == 1: - remaining -= 59 - if not self.options.coins: - remaining -= len(coins) + remaining = len(all_locations) - len(required_items) - len(self.disabled_locations) - 5 self.multiworld.itempool += [ self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining) @@ -157,21 +145,14 @@ def create_items(self) -> None: def set_rules(self) -> None: set_rules(self, self.disabled_locations) - if self.options.castle_skip: - self.multiworld.completion_condition[self.player] = lambda state: state.can_reach( - "PostJokes", "Region", self.player - ) - else: - self.multiworld.completion_condition[self.player] = lambda state: state.can_reach( - "Bowser's Castle Mini", "Region", self.player - ) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) def create_item(self, name: str) -> MLSSItem: item = item_table[name] return MLSSItem(item.itemName, item.classification, item.code, self.player) def get_filler_item_name(self) -> str: - return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))) + return self.random.choice(list(filter(lambda item: item.classification == ItemClassification.filler, itemList))).itemName def generate_output(self, output_directory: str) -> None: patch = MLSSProcedurePatch(player=self.player, player_name=self.multiworld.player_name[self.player]) diff --git a/worlds/mlss/data/basepatch.bsdiff b/worlds/mlss/data/basepatch.bsdiff index 8f9324995ec4..7ed6c38ea9f4 100644 Binary files a/worlds/mlss/data/basepatch.bsdiff and b/worlds/mlss/data/basepatch.bsdiff differ diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py index c30688f2adbe..eddd09927445 100644 --- a/worlds/mm2/rules.py +++ b/worlds/mm2/rules.py @@ -37,7 +37,7 @@ minimum_weakness_requirement: Dict[int, int] = { 0: 1, # Mega Buster is free 1: 14, # 2 shots of Atomic Fire - 2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot + 2: 2, # 14 shots of Air Shooter 3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off 4: 1, # 56 uses of Bubble Lead 5: 1, # 224 uses of Quick Boomerang diff --git a/worlds/mmbn3/__init__.py b/worlds/mmbn3/__init__.py index 97725e728bae..6d28b101c377 100644 --- a/worlds/mmbn3/__init__.py +++ b/worlds/mmbn3/__init__.py @@ -97,6 +97,28 @@ def create_regions(self) -> None: add_item_rule(loc, lambda item: not item.advancement) region.locations.append(loc) self.multiworld.regions.append(region) + + # Regions which contribute to explore score when accessible. + explore_score_region_names = ( + RegionName.WWW_Island, + RegionName.SciLab_Overworld, + RegionName.SciLab_Cyberworld, + RegionName.Yoka_Overworld, + RegionName.Yoka_Cyberworld, + RegionName.Beach_Overworld, + RegionName.Beach_Cyberworld, + RegionName.Undernet, + RegionName.Deep_Undernet, + RegionName.Secret_Area, + ) + explore_score_regions = [self.get_region(region_name) for region_name in explore_score_region_names] + + # Entrances which use explore score in their logic need to register all the explore score regions as indirect + # conditions. + def register_explore_score_indirect_conditions(entrance): + for explore_score_region in explore_score_regions: + self.multiworld.register_indirect_condition(explore_score_region, entrance) + for region_info in regions: region = name_to_region[region_info.name] for connection in region_info.connections: @@ -119,6 +141,7 @@ def create_regions(self) -> None: entrance.access_rule = lambda state: \ state.has(ItemName.CSciPas, self.player) or \ state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) + self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Yoka_Cyberworld: entrance.access_rule = lambda state: \ state.has(ItemName.CYokaPas, self.player) or \ @@ -126,16 +149,19 @@ def create_regions(self) -> None: state.can_reach(RegionName.SciLab_Overworld, "Region", self.player) and state.has(ItemName.Press, self.player) ) + self.multiworld.register_indirect_condition(self.get_region(RegionName.SciLab_Overworld), entrance) if connection == RegionName.Beach_Cyberworld: entrance.access_rule = lambda state: state.has(ItemName.CBeacPas, self.player) and\ state.can_reach(RegionName.Yoka_Overworld, "Region", self.player) - + self.multiworld.register_indirect_condition(self.get_region(RegionName.Yoka_Overworld), entrance) if connection == RegionName.Undernet: entrance.access_rule = lambda state: self.explore_score(state) > 8 and\ state.has(ItemName.Press, self.player) + register_explore_score_indirect_conditions(entrance) if connection == RegionName.Secret_Area: entrance.access_rule = lambda state: self.explore_score(state) > 12 and\ state.has(ItemName.Hammer, self.player) + register_explore_score_indirect_conditions(entrance) if connection == RegionName.WWW_Island: entrance.access_rule = lambda state:\ state.has(ItemName.Progressive_Undernet_Rank, self.player, 8) diff --git a/worlds/oot/Cosmetics.py b/worlds/oot/Cosmetics.py index f40f8a1ebb06..4a748c60aa9e 100644 --- a/worlds/oot/Cosmetics.py +++ b/worlds/oot/Cosmetics.py @@ -1,9 +1,9 @@ from .Utils import data_path, __version__ from .Colors import * import logging -import worlds.oot.Music as music -import worlds.oot.Sounds as sfx -import worlds.oot.IconManip as icon +from . import Music as music +from . import Sounds as sfx +from . import IconManip as icon from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict import json @@ -105,7 +105,7 @@ def patch_tunic_colors(rom, ootworld, symbols): # handle random if tunic_option == 'Random Choice': - tunic_option = random.choice(tunic_color_list) + tunic_option = ootworld.random.choice(tunic_color_list) # handle completely random if tunic_option == 'Completely Random': color = generate_random_color() @@ -156,9 +156,9 @@ def patch_navi_colors(rom, ootworld, symbols): # choose a random choice for the whole group if navi_option_inner == 'Random Choice': - navi_option_inner = random.choice(navi_color_list) + navi_option_inner = ootworld.random.choice(navi_color_list) if navi_option_outer == 'Random Choice': - navi_option_outer = random.choice(navi_color_list) + navi_option_outer = ootworld.random.choice(navi_color_list) if navi_option_outer == 'Match Inner': navi_option_outer = navi_option_inner @@ -233,9 +233,9 @@ def patch_sword_trails(rom, ootworld, symbols): # handle random choice if option_inner == 'Random Choice': - option_inner = random.choice(sword_trail_color_list) + option_inner = ootworld.random.choice(sword_trail_color_list) if option_outer == 'Random Choice': - option_outer = random.choice(sword_trail_color_list) + option_outer = ootworld.random.choice(sword_trail_color_list) if option_outer == 'Match Inner': option_outer = option_inner @@ -326,9 +326,9 @@ def patch_trails(rom, ootworld, trails): # handle random choice if option_inner == 'Random Choice': - option_inner = random.choice(trail_color_list) + option_inner = ootworld.random.choice(trail_color_list) if option_outer == 'Random Choice': - option_outer = random.choice(trail_color_list) + option_outer = ootworld.random.choice(trail_color_list) if option_outer == 'Match Inner': option_outer = option_inner @@ -393,7 +393,7 @@ def patch_gauntlet_colors(rom, ootworld, symbols): # handle random if gauntlet_option == 'Random Choice': - gauntlet_option = random.choice(gauntlet_color_list) + gauntlet_option = ootworld.random.choice(gauntlet_color_list) # handle completely random if gauntlet_option == 'Completely Random': color = generate_random_color() @@ -424,10 +424,10 @@ def patch_shield_frame_colors(rom, ootworld, symbols): # handle random if shield_frame_option == 'Random Choice': - shield_frame_option = random.choice(shield_frame_color_list) + shield_frame_option = ootworld.random.choice(shield_frame_color_list) # handle completely random if shield_frame_option == 'Completely Random': - color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)] + color = [ootworld.random.getrandbits(8), ootworld.random.getrandbits(8), ootworld.random.getrandbits(8)] # grab the color from the list elif shield_frame_option in shield_frame_colors: color = list(shield_frame_colors[shield_frame_option]) @@ -458,7 +458,7 @@ def patch_heart_colors(rom, ootworld, symbols): # handle random if heart_option == 'Random Choice': - heart_option = random.choice(heart_color_list) + heart_option = ootworld.random.choice(heart_color_list) # handle completely random if heart_option == 'Completely Random': color = generate_random_color() @@ -495,7 +495,7 @@ def patch_magic_colors(rom, ootworld, symbols): magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting]) if magic_option == 'Random Choice': - magic_option = random.choice(magic_color_list) + magic_option = ootworld.random.choice(magic_color_list) if magic_option == 'Completely Random': color = generate_random_color() @@ -559,7 +559,7 @@ def patch_button_colors(rom, ootworld, symbols): # handle random if button_option == 'Random Choice': - button_option = random.choice(list(button_colors.keys())) + button_option = ootworld.random.choice(list(button_colors.keys())) # handle completely random if button_option == 'Completely Random': fixed_font_color = [10, 10, 10] @@ -618,11 +618,11 @@ def patch_sfx(rom, ootworld, symbols): rom.write_int16(loc, sound_id) else: if selection == 'random-choice': - selection = random.choice(sfx.get_hook_pool(hook)).value.keyword + selection = ootworld.random.choice(sfx.get_hook_pool(hook)).value.keyword elif selection == 'random-ear-safe': - selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword + selection = ootworld.random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword elif selection == 'completely-random': - selection = random.choice(sfx.standard).value.keyword + selection = ootworld.random.choice(sfx.standard).value.keyword sound_id = sound_dict[selection] for loc in hook.value.locations: rom.write_int16(loc, sound_id) @@ -644,7 +644,7 @@ def patch_instrument(rom, ootworld, symbols): choice = ootworld.sfx_ocarina if choice == 'random-choice': - choice = random.choice(list(instruments.keys())) + choice = ootworld.random.choice(list(instruments.keys())) rom.write_byte(0x00B53C7B, instruments[choice]) rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods @@ -769,7 +769,6 @@ def patch_instrument(rom, ootworld, symbols): def patch_cosmetics(ootworld, rom): # Use the world's slot seed for cosmetics - random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player].random()) # try to detect the cosmetic patch data format versioned_patch_set = None diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index 6c4b6428f53e..8b041f045dcf 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -3,9 +3,9 @@ class OOTEntrance(Entrance): game: str = 'Ocarina of Time' - def __init__(self, player, world, name='', parent=None): + def __init__(self, player, multiworld, name='', parent=None): super(OOTEntrance, self).__init__(player, name, parent) - self.multiworld = world + self.multiworld = multiworld self.access_rules = [] self.reverse = None self.replaces = None diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index cda442ffb109..66c5df804cb4 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -440,16 +440,16 @@ class EntranceShuffleError(Exception): def shuffle_random_entrances(ootworld): - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player # Gather locations to keep reachable for validation all_state = ootworld.get_state_with_complete_itempool() all_state.sweep_for_advancements(locations=ootworld.get_locations()) - locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} + locations_to_ensure_reachable = {loc for loc in multiworld.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances - set_all_entrances_data(world, player) + set_all_entrances_data(multiworld, player) # Determine entrance pools based on settings one_way_entrance_pools = {} @@ -547,10 +547,10 @@ def shuffle_random_entrances(ootworld): none_state = CollectionState(ootworld.multiworld) # Plando entrances - if world.plando_connections[player]: + if ootworld.options.plando_connections: rollbacks = [] all_targets = {**one_way_target_entrance_pools, **target_entrance_pools} - for conn in world.plando_connections[player]: + for conn in ootworld.options.plando_connections: try: entrance = ootworld.get_entrance(conn.entrance) exit = ootworld.get_entrance(conn.exit) @@ -628,7 +628,7 @@ def shuffle_random_entrances(ootworld): logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances') # Game is beatable new_all_state = ootworld.get_state_with_complete_itempool() - if not world.has_beaten_game(new_all_state, player): + if not multiworld.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state) @@ -675,7 +675,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools): avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools)) - ootworld.multiworld.random.shuffle(avail_pool) + ootworld.random.shuffle(avail_pool) for entrance in avail_pool: if entrance.replaces: @@ -725,11 +725,11 @@ def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}') def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state): - ootworld.multiworld.random.shuffle(entrances) + ootworld.random.shuffle(entrances) for entrance in entrances: if entrance.connected_region != None: continue - ootworld.multiworld.random.shuffle(target_entrances) + ootworld.random.shuffle(target_entrances) # Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems. # success rate over randomization if pool_type in {'InteriorSoft', 'MixedSoft'}: @@ -785,7 +785,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran # TODO: improve this function def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig): - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player all_state = all_state_orig.copy() @@ -828,8 +828,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']): # Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints - potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) - potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) + potion_front = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) + potion_back = get_entrance_replacing(multiworld.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back): raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area') elif (potion_front and not potion_back) or (not potion_front and potion_back): @@ -840,8 +840,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all # When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides if ootworld.shuffle_cows: - impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) - impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) + impas_front = get_entrance_replacing(multiworld.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player) + impas_back = get_entrance_replacing(multiworld.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player) if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back): raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area') elif (impas_front and not impas_back) or (not impas_front and impas_back): @@ -861,25 +861,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)): raise EntranceShuffleError('Time passing is not guaranteed as both ages') - if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]): + if ootworld.starting_age == 'child' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]): raise EntranceShuffleError('Path to ToT as adult not guaranteed') - if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]): + if ootworld.starting_age == 'adult' and (multiworld.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]): raise EntranceShuffleError('Path to ToT as child not guaranteed') if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \ (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']): # Ensure big poe shop is always reachable as adult - if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]: + if multiworld.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]: raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult') if ootworld.shopsanity == 'off': # Ensure that Goron and Zora shops are accessible as adult - if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]: + if multiworld.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]: raise EntranceShuffleError('Goron City Shop not accessible as adult') - if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]: + if multiworld.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]: raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult') if ootworld.open_forest == 'closed': # Ensure that Kokiri Shop is reachable as child with no items - if world.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]: + if multiworld.get_region('KF Kokiri Shop', player) not in none_state.child_reachable_regions[player]: raise EntranceShuffleError('Kokiri Forest Shop not accessible as child in closed forest') diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py index b0f20858e747..28a5d37a516a 100644 --- a/worlds/oot/HintList.py +++ b/worlds/oot/HintList.py @@ -1,5 +1,3 @@ -import random - from BaseClasses import LocationProgressType from .Items import OOTItem @@ -28,7 +26,7 @@ class Hint(object): text = "" type = [] - def __init__(self, name, text, type, choice=None): + def __init__(self, name, text, type, rand, choice=None): self.name = name self.type = [type] if not isinstance(type, list) else type @@ -36,31 +34,31 @@ def __init__(self, name, text, type, choice=None): self.text = text else: if choice == None: - self.text = random.choice(text) + self.text = rand.choice(text) else: self.text = text[choice] -def getHint(item, clearer_hint=False): +def getHint(item, rand, clearer_hint=False): if item in hintTable: textOptions, clearText, hintType = hintTable[item] if clearer_hint: if clearText == None: - return Hint(item, textOptions, hintType, 0) - return Hint(item, clearText, hintType) + return Hint(item, textOptions, hintType, rand, 0) + return Hint(item, clearText, hintType, rand) else: - return Hint(item, textOptions, hintType) + return Hint(item, textOptions, hintType, rand) elif isinstance(item, str): - return Hint(item, item, 'generic') + return Hint(item, item, 'generic', rand) else: # is an Item - return Hint(item.name, item.hint_text, 'item') + return Hint(item.name, item.hint_text, 'item', rand) def getHintGroup(group, world): ret = [] for name in hintTable: - hint = getHint(name, world.clearer_hints) + hint = getHint(name, world.random, world.clearer_hints) if hint.name in world.always_hints and group == 'always': hint.type = 'always' @@ -95,7 +93,7 @@ def getHintGroup(group, world): def getRequiredHints(world): ret = [] for name in hintTable: - hint = getHint(name) + hint = getHint(name, world.random) if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world): ret.append(hint) return ret @@ -1689,7 +1687,7 @@ def hintExclusions(world, clear_cache=False): location_hints = [] for name in hintTable: - hint = getHint(name, world.clearer_hints) + hint = getHint(name, world.random, world.clearer_hints) if any(item in hint.type for item in ['always', 'dual_always', diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index e63e135e5045..c01241d04832 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -136,13 +136,13 @@ def getItemGenericName(item): def isRestrictedDungeonItem(dungeon, item): if not isinstance(item, OOTItem): return False - if (item.map or item.compass) and dungeon.multiworld.shuffle_mapcompass == 'dungeon': + if (item.map or item.compass) and dungeon.world.options.shuffle_mapcompass == 'dungeon': return item in dungeon.dungeon_items - if item.type == 'SmallKey' and dungeon.multiworld.shuffle_smallkeys == 'dungeon': + if item.type == 'SmallKey' and dungeon.world.options.shuffle_smallkeys == 'dungeon': return item in dungeon.small_keys - if item.type == 'BossKey' and dungeon.multiworld.shuffle_bosskeys == 'dungeon': + if item.type == 'BossKey' and dungeon.world.options.shuffle_bosskeys == 'dungeon': return item in dungeon.boss_key - if item.type == 'GanonBossKey' and dungeon.multiworld.shuffle_ganon_bosskey == 'dungeon': + if item.type == 'GanonBossKey' and dungeon.world.options.shuffle_ganon_bosskey == 'dungeon': return item in dungeon.boss_key return False @@ -261,8 +261,8 @@ def filterTrailingSpace(text): '', ] -def getSimpleHintNoPrefix(item): - hint = getHint(item.name, True).text +def getSimpleHintNoPrefix(item, rand): + hint = getHint(item.name, rand, True).text for prefix in hintPrefixes: if hint.startswith(prefix): @@ -417,9 +417,9 @@ def is_dungeon_item(self, item): # Formats the hint text for this area with proper grammar. # Dungeons are hinted differently depending on the clearer_hints setting. - def text(self, clearer_hints, preposition=False, world=None): + def text(self, rand, clearer_hints, preposition=False, world=None): if self.is_dungeon: - text = getHint(self.dungeon_name, clearer_hints).text + text = getHint(self.dungeon_name, rand, clearer_hints).text else: text = str(self) prefix, suffix = text.replace('#', '').split(' ', 1) @@ -489,7 +489,7 @@ def get_woth_hint(world, checked): if getattr(location.parent_region, "dungeon", None): world.woth_dungeon += 1 - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.random, world.clearer_hints).text else: location_text = get_hint_area(location) @@ -570,9 +570,9 @@ def get_good_item_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if getattr(location.parent_region, "dungeon", None): - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) else: @@ -613,10 +613,10 @@ def get_specific_item_hint(world, checked): location = world.hint_rng.choice(locations) checked[location.player].add(location.name) - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if getattr(location.parent_region, "dungeon", None): - location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text + location_text = getHint(location.parent_region.dungeon.name, world.hint_rng, world.clearer_hints).text if world.hint_dist_user.get('vague_named_items', False): return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location) else: @@ -648,9 +648,9 @@ def get_random_location_hint(world, checked): checked[location.player].add(location.name) dungeon = location.parent_region.dungeon - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text if dungeon: - location_text = getHint(dungeon.name, world.clearer_hints).text + location_text = getHint(dungeon.name, world.hint_rng, world.clearer_hints).text return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) else: @@ -675,7 +675,7 @@ def get_specific_hint(world, checked, type): location_text = hint.text if '#' not in location_text: location_text = '#%s#' % location_text - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), location) @@ -724,9 +724,9 @@ def get_entrance_hint(world, checked): connected_region = entrance.connected_region if connected_region.dungeon: - region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text + region_text = getHint(connected_region.dungeon.name, world.hint_rng, world.clearer_hints).text else: - region_text = getHint(connected_region.name, world.clearer_hints).text + region_text = getHint(connected_region.name, world.hint_rng, world.clearer_hints).text if '#' not in region_text: region_text = '#%s#' % region_text @@ -882,10 +882,10 @@ def buildWorldGossipHints(world, checkedLocations=None): if location.name in world.hint_text_overrides: location_text = world.hint_text_overrides[location.name] else: - location_text = getHint(location.name, world.clearer_hints).text + location_text = getHint(location.name, world.hint_rng, world.clearer_hints).text if '#' not in location_text: location_text = '#%s#' % location_text - item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text + item_text = getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)), ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) logging.getLogger('').debug('Placed always hint for %s.', location.name) @@ -1003,16 +1003,16 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True) ('Goron Ruby', 'Red'), ('Zora Sapphire', 'Blue'), ] - child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04' + child_text += getHint('Spiritual Stone Text Start', world.hint_rng, world.clearer_hints).text + '\x04' for (reward, color) in bossRewardsSpiritualStones: child_text += buildBossString(reward, color, world) - child_text += getHint('Child Altar Text End', world.clearer_hints).text + child_text += getHint('Child Altar Text End', world.hint_rng, world.clearer_hints).text child_text += '\x0B' update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20) # text that appears at altar as an adult. adult_text = '\x08' - adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04' + adult_text += getHint('Adult Altar Text Start', world.hint_rng, world.clearer_hints).text + '\x04' if include_rewards: bossRewardsMedallions = [ ('Light Medallion', 'Light Blue'), @@ -1029,7 +1029,7 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True) adult_text += '\x04' adult_text += buildGanonBossKeyString(world) else: - adult_text += getHint('Adult Altar Text End', world.clearer_hints).text + adult_text += getHint('Adult Altar Text End', world.hint_rng, world.clearer_hints).text adult_text += '\x0B' update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20) @@ -1044,7 +1044,7 @@ def buildBossString(reward, color, world): text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='') else: location = world.hinted_dungeon_reward_locations[reward] - location_text = HintArea.at(location).text(world.clearer_hints, preposition=True) + location_text = HintArea.at(location).text(world.hint_rng, world.clearer_hints, preposition=True) text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='') return str(text) + '\x04' @@ -1054,7 +1054,7 @@ def buildBridgeReqsString(world): if world.bridge == 'open': string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells." else: - item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text + item_req_string = getHint('bridge_' + world.bridge, world.hint_rng, world.clearer_hints).text if world.bridge == 'medallions': item_req_string = str(world.bridge_medallions) + ' ' + item_req_string elif world.bridge == 'stones': @@ -1077,7 +1077,7 @@ def buildGanonBossKeyString(world): string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#." else: if world.shuffle_ganon_bosskey == 'on_lacs': - item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text + item_req_string = getHint('lacs_' + world.lacs_condition, world.hint_rng, world.clearer_hints).text if world.lacs_condition == 'medallions': item_req_string = str(world.lacs_medallions) + ' ' + item_req_string elif world.lacs_condition == 'stones': @@ -1092,7 +1092,7 @@ def buildGanonBossKeyString(world): item_req_string = '#%s#' % item_req_string bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']: - item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text + item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text if world.shuffle_ganon_bosskey == 'medallions': item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string elif world.shuffle_ganon_bosskey == 'stones': @@ -1107,7 +1107,7 @@ def buildGanonBossKeyString(world): item_req_string = '#%s#' % item_req_string bk_location_string = "automatically granted once %s are retrieved" % item_req_string else: - bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text + bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.hint_rng, world.clearer_hints).text string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string return str(GossipText(string, ['Yellow'], prefix='')) @@ -1142,16 +1142,16 @@ def buildMiscItemHints(world, messages): if location.player != world.player: player_text = world.multiworld.get_player_name(location.player) + "'s " if location.game == 'Ocarina of Time': - area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None) + area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.hint_rng, world.clearer_hints, world=None) else: area = location.name text = data['default_item_text'].format(area=rom_safe_text(player_text + area)) elif 'default_item_fallback' in data: text = data['default_item_fallback'] else: - text = getHint('Validation Line', world.clearer_hints).text + text = getHint('Validation Line', world.hint_rng, world.clearer_hints).text location = world.get_location('Ganons Tower Boss Key Chest') - text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#" + text += f"#{getHint(getItemGenericName(location.item), world.hint_rng, world.clearer_hints).text}#" for find, replace in data.get('replace', {}).items(): text = text.replace(find, replace) @@ -1165,7 +1165,7 @@ def buildMiscLocationHints(world, messages): if hint_type in world.misc_hints: location = world.get_location(data['item_location']) item = location.item - item_text = getHint(getItemGenericName(item), world.clearer_hints).text + item_text = getHint(getItemGenericName(item), world.hint_rng, world.clearer_hints).text if item.player != world.player: item_text += f' for {world.multiworld.get_player_name(item.player)}' text = data['location_text'].format(item=rom_safe_text(item_text)) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 6ca6bc9268a9..805d1fc72dd2 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -295,16 +295,14 @@ def get_spec(tup, key, default): def get_junk_pool(ootworld): junk_pool[:] = list(junk_pool_base) - if ootworld.junk_ice_traps == 'on': + if ootworld.options.junk_ice_traps == 'on': junk_pool.append(('Ice Trap', 10)) - elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']: + elif ootworld.options.junk_ice_traps in ['mayhem', 'onslaught']: junk_pool[:] = [('Ice Trap', 1)] return junk_pool -def get_junk_item(count=1, pool=None, plando_pool=None): - global random - +def get_junk_item(rand, count=1, pool=None, plando_pool=None): if count < 1: raise ValueError("get_junk_item argument 'count' must be greater than 0.") @@ -323,17 +321,17 @@ def get_junk_item(count=1, pool=None, plando_pool=None): raise RuntimeError("Not enough junk is available in the item pool to replace removed items.") else: junk_items, junk_weights = zip(*junk_pool) - return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count)) + return_pool.extend(rand.choices(junk_items, weights=junk_weights, k=count)) return return_pool -def replace_max_item(items, item, max): +def replace_max_item(items, item, max, rand): count = 0 for i,val in enumerate(items): if val == item: if count >= max: - items[i] = get_junk_item()[0] + items[i] = get_junk_item(rand)[0] count += 1 @@ -375,7 +373,7 @@ def get_pool_core(world): pending_junk_pool.append('Kokiri Sword') if world.shuffle_ocarinas: pending_junk_pool.append('Ocarina') - if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0): + if world.shuffle_beans and world.options.start_inventory.value.get('Magic Bean Pack', 0): pending_junk_pool.append('Magic Bean Pack') if (world.gerudo_fortress != "open" and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']): @@ -450,7 +448,7 @@ def get_pool_core(world): else: item = deku_scrubs_items[location.vanilla_item] if isinstance(item, list): - item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0] + item = world.random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0] shuffle_item = True # Kokiri Sword @@ -489,7 +487,7 @@ def get_pool_core(world): # Cows elif location.vanilla_item == 'Milk': if world.shuffle_cows: - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = world.shuffle_cows if not shuffle_item: location.show_in_spoiler = False @@ -508,13 +506,13 @@ def get_pool_core(world): item = 'Rutos Letter' ruto_bottles -= 1 else: - item = random.choice(normal_bottles) + item = world.random.choice(normal_bottles) shuffle_item = True # Magic Beans elif location.vanilla_item == 'Buy Magic Bean': if world.shuffle_beans: - item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0] + item = 'Magic Bean Pack' if not world.options.start_inventory.value.get('Magic Bean Pack', 0) else get_junk_item(world.random)[0] shuffle_item = world.shuffle_beans if not shuffle_item: location.show_in_spoiler = False @@ -528,7 +526,7 @@ def get_pool_core(world): # Adult Trade Item elif location.vanilla_item == 'Pocket Egg': potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items - item = random.choice(sorted(potential_trade_items)) + item = world.random.choice(sorted(potential_trade_items)) world.selected_adult_trade_item = item shuffle_item = True @@ -541,7 +539,7 @@ def get_pool_core(world): shuffle_item = False location.show_in_spoiler = False if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings: - item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)' + item = get_junk_item(world.random)[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)' # Freestanding Rupees and Hearts elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']: @@ -618,7 +616,7 @@ def get_pool_core(world): elif dungeon.name in world.key_rings and not dungeon.small_keys: item = dungeon.item_name("Small Key Ring") elif dungeon.name in world.key_rings: - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = True # Any other item in a dungeon. elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]: @@ -630,7 +628,7 @@ def get_pool_core(world): if shuffle_setting in ['remove', 'startwith']: world.multiworld.push_precollected(dungeon_collection[-1]) world.remove_from_start_inventory.append(dungeon_collection[-1].name) - item = get_junk_item()[0] + item = get_junk_item(world.random)[0] shuffle_item = True elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']: dungeon_collection[-1].priority = True @@ -658,9 +656,9 @@ def get_pool_core(world): shop_non_item_count = len(world.shop_prices) shop_item_count = shop_slots_count - shop_non_item_count - pool.extend(random.sample(remain_shop_items, shop_item_count)) + pool.extend(world.random.sample(remain_shop_items, shop_item_count)) if shop_non_item_count: - pool.extend(get_junk_item(shop_non_item_count)) + pool.extend(get_junk_item(world.random, shop_non_item_count)) # Extra rupees for shopsanity. if world.shopsanity not in ['off', '0']: @@ -706,19 +704,19 @@ def get_pool_core(world): if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']: placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)' - pool.extend(get_junk_item()) + pool.extend(get_junk_item(world.random)) else: placed_items['Gift from Sages'] = IGNORE_LOCATION world.get_location('Gift from Sages').show_in_spoiler = False if world.junk_ice_traps == 'off': - replace_max_item(pool, 'Ice Trap', 0) + replace_max_item(pool, 'Ice Trap', 0, world.random) elif world.junk_ice_traps == 'onslaught': for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']: - replace_max_item(pool, item, 0) + replace_max_item(pool, item, 0, world.random) for item, maximum in item_difficulty_max[world.item_pool_value].items(): - replace_max_item(pool, item, maximum) + replace_max_item(pool, item, maximum, world.random) # world.distribution.alter_pool(world, pool) @@ -748,7 +746,7 @@ def get_pool_core(world): pending_item = pending_junk_pool.pop() if not junk_candidates: raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1)) - junk_item = random.choice(junk_candidates) + junk_item = world.random.choice(junk_candidates) junk_candidates.remove(junk_item) pool.remove(junk_item) pool.append(pending_item) diff --git a/worlds/oot/Messages.py b/worlds/oot/Messages.py index 25c2a9934dd4..5059c01f3c8d 100644 --- a/worlds/oot/Messages.py +++ b/worlds/oot/Messages.py @@ -1,6 +1,5 @@ # text details: https://wiki.cloudmodding.com/oot/Text_Format -import random from .HintList import misc_item_hint_table, misc_location_hint_table from .TextBox import line_wrap from .Utils import find_last @@ -969,7 +968,7 @@ def repack_messages(rom, messages, permutation=None, always_allow_skip=True, spe rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # shuffles the messages in the game, making sure to keep various message types in their own group -def shuffle_messages(messages, except_hints=True, always_allow_skip=True): +def shuffle_messages(messages, rand, except_hints=True, always_allow_skip=True): permutation = [i for i, _ in enumerate(messages)] @@ -1002,7 +1001,7 @@ def is_exempt(m): def shuffle_group(group): group_permutation = [i for i, _ in enumerate(group)] - random.shuffle(group_permutation) + rand.shuffle(group_permutation) for index_from, index_to in enumerate(group_permutation): permutation[group[index_to].index] = group[index_from].index diff --git a/worlds/oot/Music.py b/worlds/oot/Music.py index 6ed1ab54ae5d..1bb3b65aac3f 100644 --- a/worlds/oot/Music.py +++ b/worlds/oot/Music.py @@ -1,6 +1,5 @@ #Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer -import random import os from .Utils import compare_version, data_path @@ -175,7 +174,7 @@ def process_sequences(rom, sequences, target_sequences, disabled_source_sequence return sequences, target_sequences -def shuffle_music(sequences, target_sequences, music_mapping, log): +def shuffle_music(sequences, target_sequences, music_mapping, log, rand): sequence_dict = {} sequence_ids = [] @@ -191,7 +190,7 @@ def shuffle_music(sequences, target_sequences, music_mapping, log): # Shuffle the sequences if len(sequences) < len(target_sequences): raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).") - random.shuffle(sequence_ids) + rand.shuffle(sequence_ids) sequences = [] for target_sequence in target_sequences: @@ -328,7 +327,7 @@ def rebuild_sequences(rom, sequences): rom.write_byte(base, j.instrument_set) -def shuffle_pointers_table(rom, ids, music_mapping, log): +def shuffle_pointers_table(rom, ids, music_mapping, log, rand): # Read in all the Music data bgm_data = {} bgm_ids = [] @@ -341,7 +340,7 @@ def shuffle_pointers_table(rom, ids, music_mapping, log): bgm_ids.append(bgm[0]) # shuffle data - random.shuffle(bgm_ids) + rand.shuffle(bgm_ids) # Write Music data back in random ordering for bgm in ids: @@ -424,13 +423,13 @@ def randomize_music(rom, ootworld, music_mapping): # process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids) # if ootworld.background_music == 'random_custom_only': # sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()] - # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log) + # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log, ootworld.random) # if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped: # process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare') # if ootworld.fanfares == 'random_custom_only': # fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()] - # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log) + # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log, ootworld.random) # if disabled_source_sequences: # log = disable_music(rom, disabled_source_sequences.values(), log) @@ -438,10 +437,10 @@ def randomize_music(rom, ootworld, music_mapping): # rebuild_sequences(rom, sequences + fanfare_sequences) # else: if ootworld.background_music == 'randomized' or bgm_mapped: - log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log) + log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log, ootworld.random) if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped: - log = shuffle_pointers_table(rom, ff_ids, music_mapping, log) + log = shuffle_pointers_table(rom, ff_ids, music_mapping, log, ootworld.random) # end_else if disabled_target_sequences: log = disable_music(rom, disabled_target_sequences.values(), log) diff --git a/worlds/oot/N64Patch.py b/worlds/oot/N64Patch.py index 5af3279e8077..3013a94a8e3b 100644 --- a/worlds/oot/N64Patch.py +++ b/worlds/oot/N64Patch.py @@ -1,5 +1,4 @@ import struct -import random import io import array import zlib @@ -88,7 +87,7 @@ def write_block_section(start, key_skip, in_data, patch_data, is_continue): # xor_range is the range the XOR key will read from. This range is not # too important, but I tried to choose from a section that didn't really # have big gaps of 0s which we want to avoid. -def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)): +def create_patch_file(rom, rand, xor_range=(0x00B8AD30, 0x00F029A0)): dma_start, dma_end = rom.get_dma_table_range() # add header @@ -100,7 +99,7 @@ def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)): # get random xor key. This range is chosen because it generally # doesn't have many sections of 0s - xor_address = random.Random().randint(*xor_range) + xor_address = rand.randint(*xor_range) patch_data.append_int32(xor_address) new_buffer = copy.copy(rom.original.buffer) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index daf072adb59c..613c5d01b381 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,6 +1,8 @@ import typing import random -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections +from dataclasses import dataclass +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \ + PerGameCommonOptions, OptionGroup from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -1281,21 +1283,166 @@ class LogicTricks(OptionList): valid_keys_casefold = True -# All options assembled into a single dict -oot_options: typing.Dict[str, type(Option)] = { - "plando_connections": OoTPlandoConnections, - "logic_rules": Logic, - "logic_no_night_tokens_without_suns_song": NightTokens, - **open_options, - **world_options, - **bridge_options, - **dungeon_items_options, - **shuffle_options, - **timesavers_options, - **misc_options, - **itempool_options, - **cosmetic_options, - **sfx_options, - "logic_tricks": LogicTricks, - "death_link": DeathLink, -} +@dataclass +class OoTOptions(PerGameCommonOptions): + plando_connections: OoTPlandoConnections + death_link: DeathLink + logic_rules: Logic + logic_no_night_tokens_without_suns_song: NightTokens + logic_tricks: LogicTricks + open_forest: Forest + open_kakariko: Gate + open_door_of_time: DoorOfTime + zora_fountain: Fountain + gerudo_fortress: Fortress + bridge: Bridge + trials: Trials + starting_age: StartingAge + shuffle_interior_entrances: InteriorEntrances + shuffle_grotto_entrances: GrottoEntrances + shuffle_dungeon_entrances: DungeonEntrances + shuffle_overworld_entrances: OverworldEntrances + owl_drops: OwlDrops + warp_songs: WarpSongs + spawn_positions: SpawnPositions + shuffle_bosses: BossEntrances + # mix_entrance_pools: MixEntrancePools + # decouple_entrances: DecoupleEntrances + triforce_hunt: TriforceHunt + triforce_goal: TriforceGoal + extra_triforce_percentage: ExtraTriforces + bombchus_in_logic: LogicalChus + dungeon_shortcuts: DungeonShortcuts + dungeon_shortcuts_list: DungeonShortcutsList + mq_dungeons_mode: MQDungeons + mq_dungeons_list: MQDungeonList + mq_dungeons_count: MQDungeonCount + # empty_dungeons_mode: EmptyDungeons + # empty_dungeons_list: EmptyDungeonList + # empty_dungeon_count: EmptyDungeonCount + bridge_stones: BridgeStones + bridge_medallions: BridgeMedallions + bridge_rewards: BridgeRewards + bridge_tokens: BridgeTokens + bridge_hearts: BridgeHearts + shuffle_mapcompass: ShuffleMapCompass + shuffle_smallkeys: ShuffleKeys + shuffle_hideoutkeys: ShuffleGerudoKeys + shuffle_bosskeys: ShuffleBossKeys + enhance_map_compass: EnhanceMC + shuffle_ganon_bosskey: ShuffleGanonBK + ganon_bosskey_medallions: GanonBKMedallions + ganon_bosskey_stones: GanonBKStones + ganon_bosskey_rewards: GanonBKRewards + ganon_bosskey_tokens: GanonBKTokens + ganon_bosskey_hearts: GanonBKHearts + key_rings: KeyRings + key_rings_list: KeyRingList + shuffle_song_items: SongShuffle + shopsanity: ShopShuffle + shop_slots: ShopSlots + shopsanity_prices: ShopPrices + tokensanity: TokenShuffle + shuffle_scrubs: ScrubShuffle + shuffle_child_trade: ShuffleChildTrade + shuffle_freestanding_items: ShuffleFreestanding + shuffle_pots: ShufflePots + shuffle_crates: ShuffleCrates + shuffle_cows: ShuffleCows + shuffle_beehives: ShuffleBeehives + shuffle_kokiri_sword: ShuffleSword + shuffle_ocarinas: ShuffleOcarinas + shuffle_gerudo_card: ShuffleCard + shuffle_beans: ShuffleBeans + shuffle_medigoron_carpet_salesman: ShuffleMedigoronCarpet + shuffle_frog_song_rupees: ShuffleFrogRupees + no_escape_sequence: SkipEscape + no_guard_stealth: SkipStealth + no_epona_race: SkipEponaRace + skip_some_minigame_phases: SkipMinigamePhases + complete_mask_quest: CompleteMaskQuest + useful_cutscenes: UsefulCutscenes + fast_chests: FastChests + free_scarecrow: FreeScarecrow + fast_bunny_hood: FastBunny + plant_beans: PlantBeans + chicken_count: ChickenCount + big_poe_count: BigPoeCount + fae_torch_count: FAETorchCount + correct_chest_appearances: CorrectChestAppearance + minor_items_as_major_chest: MinorInMajor + invisible_chests: InvisibleChests + correct_potcrate_appearances: CorrectPotCrateAppearance + hints: Hints + misc_hints: MiscHints + hint_dist: HintDistribution + text_shuffle: TextShuffle + damage_multiplier: DamageMultiplier + deadly_bonks: DeadlyBonks + no_collectible_hearts: HeroMode + starting_tod: StartingToD + blue_fire_arrows: BlueFireArrows + fix_broken_drops: FixBrokenDrops + start_with_consumables: ConsumableStart + start_with_rupees: RupeeStart + item_pool_value: ItemPoolValue + junk_ice_traps: IceTraps + ice_trap_appearance: IceTrapVisual + adult_trade_start: AdultTradeStart + default_targeting: Targeting + display_dpad: DisplayDpad + dpad_dungeon_menu: DpadDungeonMenu + correct_model_colors: CorrectColors + background_music: BackgroundMusic + fanfares: Fanfares + ocarina_fanfares: OcarinaFanfares + kokiri_color: kokiri_color + goron_color: goron_color + zora_color: zora_color + silver_gauntlets_color: silver_gauntlets_color + golden_gauntlets_color: golden_gauntlets_color + mirror_shield_frame_color: mirror_shield_frame_color + navi_color_default_inner: navi_color_default_inner + navi_color_default_outer: navi_color_default_outer + navi_color_enemy_inner: navi_color_enemy_inner + navi_color_enemy_outer: navi_color_enemy_outer + navi_color_npc_inner: navi_color_npc_inner + navi_color_npc_outer: navi_color_npc_outer + navi_color_prop_inner: navi_color_prop_inner + navi_color_prop_outer: navi_color_prop_outer + sword_trail_duration: SwordTrailDuration + sword_trail_color_inner: sword_trail_color_inner + sword_trail_color_outer: sword_trail_color_outer + bombchu_trail_color_inner: bombchu_trail_color_inner + bombchu_trail_color_outer: bombchu_trail_color_outer + boomerang_trail_color_inner: boomerang_trail_color_inner + boomerang_trail_color_outer: boomerang_trail_color_outer + heart_color: heart_color + magic_color: magic_color + a_button_color: a_button_color + b_button_color: b_button_color + c_button_color: c_button_color + start_button_color: start_button_color + sfx_navi_overworld: sfx_navi_overworld + sfx_navi_enemy: sfx_navi_enemy + sfx_low_hp: sfx_low_hp + sfx_menu_cursor: sfx_menu_cursor + sfx_menu_select: sfx_menu_select + sfx_nightfall: sfx_nightfall + sfx_horse_neigh: sfx_horse_neigh + sfx_hover_boots: sfx_hover_boots + sfx_ocarina: SfxOcarina + + +oot_option_groups: typing.List[OptionGroup] = [ + OptionGroup("Open", [option for option in open_options.values()]), + OptionGroup("World", [*[option for option in world_options.values()], + *[option for option in bridge_options.values()]]), + OptionGroup("Shuffle", [option for option in shuffle_options.values()]), + OptionGroup("Dungeon Items", [option for option in dungeon_items_options.values()]), + OptionGroup("Timesavers", [option for option in timesavers_options.values()]), + OptionGroup("Misc", [option for option in misc_options.values()]), + OptionGroup("Item Pool", [option for option in itempool_options.values()]), + OptionGroup("Cosmetics", [option for option in cosmetic_options.values()]), + OptionGroup("SFX", [option for option in sfx_options.values()]) +] diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 2219d7bb95a8..561d7c3f7b6e 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -208,8 +208,8 @@ def patch_rom(world, rom): # Fix Ice Cavern Alcove Camera if not world.dungeon_mq['Ice Cavern']: - rom.write_byte(0x2BECA25,0x01); - rom.write_byte(0x2BECA2D,0x01); + rom.write_byte(0x2BECA25,0x01) + rom.write_byte(0x2BECA2D,0x01) # Fix GS rewards to be static rom.write_int32(0xEA3934, 0) @@ -944,7 +944,7 @@ def add_scene_exits(scene_start, offset = 0): scene_table = 0x00B71440 for scene in range(0x00, 0x65): - scene_start = rom.read_int32(scene_table + (scene * 0x14)); + scene_start = rom.read_int32(scene_table + (scene * 0x14)) add_scene_exits(scene_start) return exit_table @@ -1632,10 +1632,10 @@ def set_entrance_updates(entrances): reward_text = None elif getattr(location.item, 'looks_like_item', None) is not None: jabu_item = location.item.looks_like_item - reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), True).text) + reward_text = create_fake_name(getHint(getItemGenericName(location.item.looks_like_item), world.hint_rng, True).text) else: jabu_item = location.item - reward_text = getHint(getItemGenericName(location.item), True).text + reward_text = getHint(getItemGenericName(location.item), world.hint_rng, True).text # Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu if reward_text is None: @@ -1687,7 +1687,7 @@ def set_entrance_updates(entrances): # Sets hooks for gossip stone changes - symbol = rom.sym("GOSSIP_HINT_CONDITION"); + symbol = rom.sym("GOSSIP_HINT_CONDITION") if world.hints == 'none': rom.write_int32(symbol, 0) @@ -2264,9 +2264,9 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name # text shuffle if world.text_shuffle == 'except_hints': - permutation = shuffle_messages(messages, except_hints=True) + permutation = shuffle_messages(messages, world.random, except_hints=True) elif world.text_shuffle == 'complete': - permutation = shuffle_messages(messages, except_hints=False) + permutation = shuffle_messages(messages, world.random, except_hints=False) # update warp song preview text boxes update_warp_song_text(messages, world) @@ -2358,7 +2358,7 @@ def update_scrub_text(message, text_replacement, default_price, price, item_name # Write numeric seed truncated to 32 bits for rng seeding # Overwritten with new seed every time a new rng value is generated - rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32) + rng_seed = world.random.getrandbits(32) rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed) # Static initial seed value for one-time random actions like the Hylian Shield discount rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed) @@ -2560,7 +2560,7 @@ def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, process room_count = rom.read_byte(scene_data + 1) room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF) for _ in range(0, room_count): - room_data = rom.read_int32(room_list); + room_data = rom.read_int32(room_list) if not room_data in processed_rooms: actors.update(room_get_actors(rom, actor_func, room_data, scene)) @@ -2591,7 +2591,7 @@ def get_actor_list(rom, actor_func): actors = {} scene_table = 0x00B71440 for scene in range(0x00, 0x65): - scene_data = rom.read_int32(scene_table + (scene * 0x14)); + scene_data = rom.read_int32(scene_table + (scene * 0x14)) actors.update(scene_get_actors(rom, actor_func, scene_data, scene)) return actors @@ -2605,7 +2605,7 @@ def get_override_itemid(override_table, scene, type, flags): def remove_entrance_blockers(rom): def remove_entrance_blockers_do(rom, actor_id, actor, scene): if actor_id == 0x014E and scene == 97: - actor_var = rom.read_int16(actor + 14); + actor_var = rom.read_int16(actor + 14) if actor_var == 0xFF01: rom.write_int16(actor + 14, 0x0700) get_actor_list(rom, remove_entrance_blockers_do) @@ -2789,7 +2789,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1]) else: if item_display.game == "Ocarina of Time": - shop_item_name = getSimpleHintNoPrefix(item_display) + shop_item_name = getSimpleHintNoPrefix(item_display, world.random) else: shop_item_name = item_display.name diff --git a/worlds/oot/Regions.py b/worlds/oot/Regions.py index 5d5cc9b13822..4a3d7e416a15 100644 --- a/worlds/oot/Regions.py +++ b/worlds/oot/Regions.py @@ -64,7 +64,7 @@ def get_scene(self): return None def can_reach(self, state): - if state.stale[self.player]: + if state._oot_stale[self.player]: stored_age = state.age[self.player] state._oot_update_age_reachable_regions(self.player) state.age[self.player] = stored_age diff --git a/worlds/oot/RuleParser.py b/worlds/oot/RuleParser.py index 0791ad5d1a3f..e5390474b779 100644 --- a/worlds/oot/RuleParser.py +++ b/worlds/oot/RuleParser.py @@ -53,7 +53,7 @@ def isliteral(expr): class Rule_AST_Transformer(ast.NodeTransformer): def __init__(self, world, player): - self.multiworld = world + self.world = world self.player = player self.events = set() # map Region -> rule ast string -> item name @@ -86,9 +86,9 @@ def visit_Name(self, node): ctx=ast.Load()), args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)], keywords=[]) - elif node.id in self.multiworld.__dict__: + elif node.id in self.world.__dict__: # Settings are constant - return ast.parse('%r' % self.multiworld.__dict__[node.id], mode='eval').body + return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body elif node.id in State.__dict__: return self.make_call(node, node.id, [], []) elif node.id in self.kwarg_defaults or node.id in allowed_globals: @@ -137,7 +137,7 @@ def visit_Tuple(self, node): if isinstance(count, ast.Name): # Must be a settings constant - count = ast.parse('%r' % self.multiworld.__dict__[count.id], mode='eval').body + count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body if iname in escaped_items: iname = escaped_items[iname] @@ -182,7 +182,7 @@ def visit_Call(self, node): new_args = [] for child in node.args: if isinstance(child, ast.Name): - if child.id in self.multiworld.__dict__: + if child.id in self.world.__dict__: # child = ast.Attribute( # value=ast.Attribute( # value=ast.Name(id='state', ctx=ast.Load()), @@ -190,7 +190,7 @@ def visit_Call(self, node): # ctx=ast.Load()), # attr=child.id, # ctx=ast.Load()) - child = ast.Constant(getattr(self.multiworld, child.id)) + child = ast.Constant(getattr(self.world, child.id)) elif child.id in rule_aliases: child = self.visit(child) elif child.id in escaped_items: @@ -242,7 +242,7 @@ def escape_or_string(n): # Fast check for json can_use if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq) and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name) - and node.left.id not in self.multiworld.__dict__ and node.comparators[0].id not in self.multiworld.__dict__): + and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__): return ast.NameConstant(node.left.id == node.comparators[0].id) node.left = escape_or_string(node.left) @@ -378,7 +378,7 @@ def replace_subrule(self, target, node): # Requires the target regions have been defined in the world. def create_delayed_rules(self): for region_name, node, subrule_name in self.delayed_rules: - region = self.multiworld.multiworld.get_region(region_name, self.player) + region = self.world.multiworld.get_region(region_name, self.player) event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True) event.show_in_spoiler = False @@ -395,7 +395,7 @@ def create_delayed_rules(self): set_rule(event, access_rule) region.locations.append(event) - self.multiworld.make_event_item(subrule_name, event) + self.world.make_event_item(subrule_name, event) # Safeguard in case this is called multiple times per world self.delayed_rules.clear() @@ -448,7 +448,7 @@ def here(self, node): ## Handlers for compile-time optimizations (former State functions) def at_day(self, node): - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAY or (tod == NONE and (ss or find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -456,7 +456,7 @@ def at_day(self, node): return ast.NameConstant(True) def at_dampe_time(self, node): - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAMPE or (tod == NONE and (find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -464,10 +464,10 @@ def at_dampe_time(self, node): return ast.NameConstant(True) def at_night(self, node): - if self.current_spot.type == 'GS Token' and self.multiworld.logic_no_night_tokens_without_suns_song: + if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song: # Using visit here to resolve 'can_play' rule return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body) - if self.multiworld.ensure_tod_access: + if self.world.ensure_tod_access: # tod has DAMPE or (tod == NONE and (ss or find a path from a provider)) # parsing is better than constructing this expression by hand r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region @@ -501,7 +501,7 @@ def current_spot_adult_access(self, node): return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body def current_spot_starting_age_access(self, node): - return self.current_spot_child_access(node) if self.multiworld.starting_age == 'child' else self.current_spot_adult_access(node) + return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node) def has_bottle(self, node): return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index 4bbf15435cfe..00f4aeb4b7d5 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -8,12 +8,17 @@ from .Items import oot_is_item_of_type from .LocationList import dungeon_song_locations -from BaseClasses import CollectionState +from BaseClasses import CollectionState, MultiWorld from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item -from ..AutoWorld import LogicMixin +from worlds.AutoWorld import LogicMixin class OOTLogic(LogicMixin): + def init_mixin(self, parent: MultiWorld): + # Separate stale state for OOTRegion.can_reach() to use because CollectionState.update_reachable_regions() sets + # `self.state[player] = False` for all players without updating OOT's age region accessibility. + self._oot_stale = {player: True for player, world in parent.worlds.items() + if parent.worlds[player].game == "Ocarina of Time"} def _oot_has_stones(self, count, player): return self.has_group("stones", player, count) @@ -92,9 +97,9 @@ def _oot_reach_at_time(self, regionname, tod, already_checked, player): return False # Store the age before calling this! - def _oot_update_age_reachable_regions(self, player): - self.stale[player] = False - for age in ['child', 'adult']: + def _oot_update_age_reachable_regions(self, player): + self._oot_stale[player] = False + for age in ['child', 'adult']: self.age[player] = age rrp = getattr(self, f'{age}_reachable_regions')[player] bc = getattr(self, f'{age}_blocked_connections')[player] @@ -127,17 +132,17 @@ def _oot_update_age_reachable_regions(self, player): def set_rules(ootworld): logger = logging.getLogger('') - world = ootworld.multiworld + multiworld = ootworld.multiworld player = ootworld.player if ootworld.logic_rules != 'no_logic': if ootworld.triforce_hunt: - world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal) + multiworld.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal) else: - world.completion_condition[player] = lambda state: state.has('Triforce', player) + multiworld.completion_condition[player] = lambda state: state.has('Triforce', player) # ganon can only carry triforce - world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce' + multiworld.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce' # is_child = ootworld.parser.parse_rule('is_child') guarantee_hint = ootworld.parser.parse_rule('guarantee_hint') @@ -151,22 +156,22 @@ def set_rules(ootworld): if (ootworld.dungeon_mq['Forest Temple'] and ootworld.shuffle_bosskeys == 'dungeon' and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off'): # First room chest needs to be a small key. Make sure the boss key isn't placed here. - location = world.get_location('Forest Temple MQ First Room Chest', player) + location = multiworld.get_location('Forest Temple MQ First Room Chest', player) forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player) if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items: # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. - location = world.get_location('Sheik in Ice Cavern', player) + location = multiworld.get_location('Sheik in Ice Cavern', player) add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song')) if ootworld.shuffle_child_trade == 'skip_child_zelda': # Song from Impa must be local - location = world.get_location('Song from Impa', player) + location = multiworld.get_location('Song from Impa', player) add_item_rule(location, lambda item: item.player == player) for name in ootworld.always_hints: - add_rule(world.get_location(name, player), guarantee_hint) + add_rule(multiworld.get_location(name, player), guarantee_hint) # TODO: re-add hints once they are working # if location.type == 'HintStone' and ootworld.hints == 'mask': diff --git a/worlds/oot/TextBox.py b/worlds/oot/TextBox.py index a9db47996299..e502d739048f 100644 --- a/worlds/oot/TextBox.py +++ b/worlds/oot/TextBox.py @@ -1,4 +1,4 @@ -import worlds.oot.Messages as Messages +from . import Messages # Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the # characters on a line reach this value. diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index ee78958b2dbe..b93f60b2a08e 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -20,7 +20,7 @@ from .Regions import OOTRegion, TimeOfDay from .Rules import set_rules, set_shop_rules, set_entrances_based_rules from .RuleParser import Rule_AST_Transformer -from .Options import oot_options +from .Options import OoTOptions, oot_option_groups from .Utils import data_path, read_json from .LocationList import business_scrubs, set_drop_location_names, dungeon_song_locations from .DungeonList import dungeon_table, create_dungeons @@ -30,12 +30,12 @@ from .N64Patch import create_patch_file from .Cosmetics import patch_cosmetics -from Utils import get_options +from settings import get_settings from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule -from ..AutoWorld import World, AutoLogicRegister, WebWorld +from worlds.AutoWorld import World, AutoLogicRegister, WebWorld # OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory. i_o_limiter = threading.Semaphore(2) @@ -128,6 +128,7 @@ class OOTWeb(WebWorld): ) tutorials = [setup, setup_es, setup_fr, setup_de] + option_groups = oot_option_groups class OOTWorld(World): @@ -137,7 +138,8 @@ class OOTWorld(World): to rescue the Seven Sages, and then confront Ganondorf to save Hyrule! """ game: str = "Ocarina of Time" - option_definitions: dict = oot_options + options_dataclass = OoTOptions + options: OoTOptions settings: typing.ClassVar[OOTSettings] topology_present: bool = True item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if @@ -195,15 +197,15 @@ def __init__(self, world, player): @classmethod def stage_assert_generate(cls, multiworld: MultiWorld): - rom = Rom(file=get_options()['oot_options']['rom_file']) + rom = Rom(file=get_settings()['oot_options']['rom_file']) # Option parsing, handling incompatible options, building useful-item table def generate_early(self): self.parser = Rule_AST_Transformer(self, self.player) - for (option_name, option) in oot_options.items(): - result = getattr(self.multiworld, option_name)[self.player] + for option_name in self.options_dataclass.type_hints: + result = getattr(self.options, option_name) if isinstance(result, Range): option_value = int(result) elif isinstance(result, Toggle): @@ -223,8 +225,8 @@ def generate_early(self): self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False - self.file_hash = [self.multiworld.random.randint(0, 31) for i in range(5)] - self.connect_name = ''.join(self.multiworld.random.choices(printable, k=16)) + self.file_hash = [self.random.randint(0, 31) for i in range(5)] + self.connect_name = ''.join(self.random.choices(printable, k=16)) self.collectible_flag_addresses = {} # Incompatible option handling @@ -283,7 +285,7 @@ def generate_early(self): local_types.append('BossKey') if self.shuffle_ganon_bosskey != 'keysanity': local_types.append('GanonBossKey') - self.multiworld.local_items[self.player].value |= set(name for name, data in item_table.items() if data[0] in local_types) + self.options.local_items.value |= set(name for name, data in item_table.items() if data[0] in local_types) # If any songs are itemlinked, set songs_as_items for group in self.multiworld.groups.values(): @@ -297,7 +299,7 @@ def generate_early(self): # Determine skipped trials in GT # This needs to be done before the logic rules in GT are parsed trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light'] - chosen_trials = self.multiworld.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip + chosen_trials = self.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list} # Determine tricks in logic @@ -311,8 +313,8 @@ def generate_early(self): # No Logic forces all tricks on, prog balancing off and beatable-only elif self.logic_rules == 'no_logic': - self.multiworld.progression_balancing[self.player].value = False - self.multiworld.accessibility[self.player].value = Accessibility.option_minimal + self.options.progression_balancing.value = False + self.options.accessibility.value = Accessibility.option_minimal for trick in normalized_name_tricks.values(): setattr(self, trick['name'], True) @@ -333,8 +335,8 @@ def generate_early(self): # Set internal names used by the OoT generator self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] - self.trials_random = self.multiworld.trials[self.player].randomized - self.mq_dungeons_random = self.multiworld.mq_dungeons_count[self.player].randomized + self.trials_random = self.options.trials.randomized + self.mq_dungeons_random = self.options.mq_dungeons_count.randomized self.easier_fire_arrow_entry = self.fae_torch_count < 24 if self.misc_hints: @@ -393,8 +395,8 @@ def generate_early(self): elif self.key_rings == 'choose': self.key_rings = self.key_rings_list elif self.key_rings == 'random_dungeons': - self.key_rings = self.multiworld.random.sample(keyring_dungeons, - self.multiworld.random.randint(0, len(keyring_dungeons))) + self.key_rings = self.random.sample(keyring_dungeons, + self.random.randint(0, len(keyring_dungeons))) # Determine which dungeons are MQ. Not compatible with glitched logic. mq_dungeons = set() @@ -405,7 +407,7 @@ def generate_early(self): elif self.mq_dungeons_mode == 'specific': mq_dungeons = self.mq_dungeons_specific elif self.mq_dungeons_mode == 'count': - mq_dungeons = self.multiworld.random.sample(all_dungeons, self.mq_dungeons_count) + mq_dungeons = self.random.sample(all_dungeons, self.mq_dungeons_count) else: self.mq_dungeons_mode = 'count' self.mq_dungeons_count = 0 @@ -425,8 +427,8 @@ def generate_early(self): elif self.dungeon_shortcuts_choice == 'all': self.dungeon_shortcuts = set(shortcut_dungeons) elif self.dungeon_shortcuts_choice == 'random': - self.dungeon_shortcuts = self.multiworld.random.sample(shortcut_dungeons, - self.multiworld.random.randint(0, len(shortcut_dungeons))) + self.dungeon_shortcuts = self.random.sample(shortcut_dungeons, + self.random.randint(0, len(shortcut_dungeons))) # == 'choice', leave as previous else: self.dungeon_shortcuts = set() @@ -576,7 +578,7 @@ def load_regions_from_json(self, file_path): new_exit = OOTEntrance(self.player, self.multiworld, '%s -> %s' % (new_region.name, exit), new_region) new_exit.vanilla_connected_region = exit new_exit.rule_string = rule - if self.multiworld.logic_rules != 'none': + if self.options.logic_rules != 'no_logic': self.parser.parse_spot_rule(new_exit) if new_exit.never: logger.debug('Dropping unreachable exit: %s', new_exit.name) @@ -607,7 +609,7 @@ def set_scrub_prices(self): elif self.shuffle_scrubs == 'random': # this is a random value between 0-99 # average value is ~33 rupees - price = int(self.multiworld.random.betavariate(1, 2) * 99) + price = int(self.random.betavariate(1, 2) * 99) # Set price in the dictionary as well as the location. self.scrub_prices[scrub_item] = price @@ -624,7 +626,7 @@ def random_shop_prices(self): self.shop_prices = {} for region in self.regions: if self.shopsanity == 'random': - shop_item_count = self.multiworld.random.randint(0, 4) + shop_item_count = self.random.randint(0, 4) else: shop_item_count = int(self.shopsanity) @@ -632,17 +634,17 @@ def random_shop_prices(self): if location.type == 'Shop': if location.name[-1:] in shop_item_indexes[:shop_item_count]: if self.shopsanity_prices == 'normal': - self.shop_prices[location.name] = int(self.multiworld.random.betavariate(1.5, 2) * 60) * 5 + self.shop_prices[location.name] = int(self.random.betavariate(1.5, 2) * 60) * 5 elif self.shopsanity_prices == 'affordable': self.shop_prices[location.name] = 10 elif self.shopsanity_prices == 'starting_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,100,5) + self.shop_prices[location.name] = self.random.randrange(0,100,5) elif self.shopsanity_prices == 'adults_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,201,5) + self.shop_prices[location.name] = self.random.randrange(0,201,5) elif self.shopsanity_prices == 'giants_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,501,5) + self.shop_prices[location.name] = self.random.randrange(0,501,5) elif self.shopsanity_prices == 'tycoons_wallet': - self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5) + self.shop_prices[location.name] = self.random.randrange(0,1000,5) # Fill boss prizes @@ -667,8 +669,8 @@ def fill_bosses(self, bossCount=9): while bossCount: bossCount -= 1 - self.multiworld.random.shuffle(prizepool) - self.multiworld.random.shuffle(prize_locs) + self.random.shuffle(prizepool) + self.random.shuffle(prize_locs) item = prizepool.pop() loc = prize_locs.pop() loc.place_locked_item(item) @@ -778,7 +780,7 @@ def create_items(self): # Call the junk fill and get a replacement if item in self.itempool: self.itempool.remove(item) - self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool))) + self.itempool.append(self.create_item(*get_junk_item(self.random, pool=junk_pool))) if self.start_with_consumables: self.starting_items['Deku Sticks'] = 30 self.starting_items['Deku Nuts'] = 40 @@ -881,7 +883,7 @@ def prefill_state(base_state): # Prefill shops, songs, and dungeon items items = self.get_pre_fill_items() locations = list(self.multiworld.get_unfilled_locations(self.player)) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) # Set up initial state state = CollectionState(self.multiworld) @@ -910,7 +912,7 @@ def prefill_state(base_state): if isinstance(locations, list): for item in stage_items: self.pre_fill_items.remove(item) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items, single_player_placement=True, lock=True, allow_excluded=True) else: @@ -923,7 +925,7 @@ def prefill_state(base_state): if isinstance(locations, list): for item in dungeon_items: self.pre_fill_items.remove(item) - self.multiworld.random.shuffle(locations) + self.random.shuffle(locations) fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) @@ -964,7 +966,7 @@ def prefill_state(base_state): while tries: try: - self.multiworld.random.shuffle(song_locations) + self.random.shuffle(song_locations) fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:], single_player_placement=True, lock=True, allow_excluded=True) logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") @@ -996,7 +998,7 @@ def prefill_state(base_state): 'Buy Goron Tunic': 1, 'Buy Zora Tunic': 1, }.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement - self.multiworld.random.shuffle(shop_locations) + self.random.shuffle(shop_locations) self.pre_fill_items = [] # all prefill should be done fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog, single_player_placement=True, lock=True, allow_excluded=True) @@ -1028,7 +1030,7 @@ def prefill_state(base_state): ganon_junk_fill = min(1, ganon_junk_fill) gc = next(filter(lambda dungeon: dungeon.name == 'Ganons Castle', self.dungeons)) locations = [loc.name for region in gc.regions for loc in region.locations if loc.item is None] - junk_fill_locations = self.multiworld.random.sample(locations, round(len(locations) * ganon_junk_fill)) + junk_fill_locations = self.random.sample(locations, round(len(locations) * ganon_junk_fill)) exclusion_rules(self.multiworld, self.player, junk_fill_locations) # Locations which are not sendable must be converted to events @@ -1074,13 +1076,13 @@ def generate_output(self, output_directory: str): trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap] self.trap_appearances = {} for loc_id in trap_location_ids: - self.trap_appearances[loc_id] = self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.fake_items).name) + self.trap_appearances[loc_id] = self.create_item(self.random.choice(self.fake_items).name) # Seed hint RNG, used for ganon text lines also - self.hint_rng = self.multiworld.per_slot_randoms[self.player] + self.hint_rng = self.random outfile_name = self.multiworld.get_out_file_name_base(self.player) - rom = Rom(file=get_options()['oot_options']['rom_file']) + rom = Rom(file=get_settings()['oot_options']['rom_file']) try: if self.hints != 'none': buildWorldGossipHints(self) @@ -1092,7 +1094,7 @@ def generate_output(self, output_directory: str): finally: self.collectible_flags_available.set() rom.update_header() - patch_data = create_patch_file(rom) + patch_data = create_patch_file(rom, self.random) rom.restore() apz5 = OoTContainer(patch_data, outfile_name, output_directory, @@ -1301,6 +1303,7 @@ def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: # the appropriate number of keys in the collection state when they are # picked up. def collect(self, state: CollectionState, item: OOTItem) -> bool: + state._oot_stale[self.player] = True if item.advancement and item.special and item.special.get('alias', False): alt_item_name, count = item.special.get('alias') state.prog_items[self.player][alt_item_name] += count @@ -1313,8 +1316,12 @@ def remove(self, state: CollectionState, item: OOTItem) -> bool: state.prog_items[self.player][alt_item_name] -= count if state.prog_items[self.player][alt_item_name] < 1: del (state.prog_items[self.player][alt_item_name]) + state._oot_stale[self.player] = True return True - return super().remove(state, item) + changed = super().remove(state, item) + if changed: + state._oot_stale[self.player] = True + return changed # Helper functions @@ -1389,12 +1396,12 @@ def get_state_with_complete_itempool(self): # If free_scarecrow give Scarecrow Song if self.free_scarecrow: all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=True) - all_state.stale[self.player] = True + all_state._oot_stale[self.player] = True return all_state def get_filler_item_name(self) -> str: - return get_junk_item(count=1, pool=get_junk_pool(self))[0] + return get_junk_item(self.random, count=1, pool=get_junk_pool(self))[0] def valid_dungeon_item_location(world: OOTWorld, option: str, dungeon: str, loc: OOTLocation) -> bool: diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py index 520cd8e8b06b..81e017eddb34 100644 --- a/worlds/osrs/Options.py +++ b/worlds/osrs/Options.py @@ -63,6 +63,7 @@ class MaxCombatLevel(Range): The highest combat level of monster to possibly be assigned as a task. If set to 0, no combat tasks will be generated. """ + display_name = "Max Required Enemy Combat Level" range_start = 0 range_end = 1520 default = 50 @@ -74,6 +75,7 @@ class MaxCombatTasks(Range): If set to 0, no combat tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Combat Task Count" range_start = 0 range_end = MAX_COMBAT_TASKS default = MAX_COMBAT_TASKS @@ -85,6 +87,7 @@ class CombatTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Combat Task Weight" range_start = 0 range_end = 99 default = 50 @@ -95,6 +98,7 @@ class MaxPrayerLevel(Range): The highest Prayer requirement of any task generated. If set to 0, no Prayer tasks will be generated. """ + display_name = "Max Required Prayer Level" range_start = 0 range_end = 99 default = 50 @@ -106,6 +110,7 @@ class MaxPrayerTasks(Range): If set to 0, no Prayer tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Prayer Task Count" range_start = 0 range_end = MAX_PRAYER_TASKS default = MAX_PRAYER_TASKS @@ -117,6 +122,7 @@ class PrayerTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Prayer Task Weight" range_start = 0 range_end = 99 default = 50 @@ -127,6 +133,7 @@ class MaxMagicLevel(Range): The highest Magic requirement of any task generated. If set to 0, no Magic tasks will be generated. """ + display_name = "Max Required Magic Level" range_start = 0 range_end = 99 default = 50 @@ -138,6 +145,7 @@ class MaxMagicTasks(Range): If set to 0, no Magic tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Magic Task Count" range_start = 0 range_end = MAX_MAGIC_TASKS default = MAX_MAGIC_TASKS @@ -149,6 +157,7 @@ class MagicTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Magic Task Weight" range_start = 0 range_end = 99 default = 50 @@ -159,6 +168,7 @@ class MaxRunecraftLevel(Range): The highest Runecraft requirement of any task generated. If set to 0, no Runecraft tasks will be generated. """ + display_name = "Max Required Runecraft Level" range_start = 0 range_end = 99 default = 50 @@ -170,6 +180,7 @@ class MaxRunecraftTasks(Range): If set to 0, no Runecraft tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Runecraft Task Count" range_start = 0 range_end = MAX_RUNECRAFT_TASKS default = MAX_RUNECRAFT_TASKS @@ -181,6 +192,7 @@ class RunecraftTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Runecraft Task Weight" range_start = 0 range_end = 99 default = 50 @@ -191,6 +203,7 @@ class MaxCraftingLevel(Range): The highest Crafting requirement of any task generated. If set to 0, no Crafting tasks will be generated. """ + display_name = "Max Required Crafting Level" range_start = 0 range_end = 99 default = 50 @@ -202,6 +215,7 @@ class MaxCraftingTasks(Range): If set to 0, no Crafting tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Crafting Task Count" range_start = 0 range_end = MAX_CRAFTING_TASKS default = MAX_CRAFTING_TASKS @@ -213,6 +227,7 @@ class CraftingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Crafting Task Weight" range_start = 0 range_end = 99 default = 50 @@ -223,6 +238,7 @@ class MaxMiningLevel(Range): The highest Mining requirement of any task generated. If set to 0, no Mining tasks will be generated. """ + display_name = "Max Required Mining Level" range_start = 0 range_end = 99 default = 50 @@ -234,6 +250,7 @@ class MaxMiningTasks(Range): If set to 0, no Mining tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Mining Task Count" range_start = 0 range_end = MAX_MINING_TASKS default = MAX_MINING_TASKS @@ -245,6 +262,7 @@ class MiningTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Mining Task Weight" range_start = 0 range_end = 99 default = 50 @@ -255,6 +273,7 @@ class MaxSmithingLevel(Range): The highest Smithing requirement of any task generated. If set to 0, no Smithing tasks will be generated. """ + display_name = "Max Required Smithing Level" range_start = 0 range_end = 99 default = 50 @@ -266,6 +285,7 @@ class MaxSmithingTasks(Range): If set to 0, no Smithing tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Smithing Task Count" range_start = 0 range_end = MAX_SMITHING_TASKS default = MAX_SMITHING_TASKS @@ -277,6 +297,7 @@ class SmithingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Smithing Task Weight" range_start = 0 range_end = 99 default = 50 @@ -287,6 +308,7 @@ class MaxFishingLevel(Range): The highest Fishing requirement of any task generated. If set to 0, no Fishing tasks will be generated. """ + display_name = "Max Required Fishing Level" range_start = 0 range_end = 99 default = 50 @@ -298,6 +320,7 @@ class MaxFishingTasks(Range): If set to 0, no Fishing tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Fishing Task Count" range_start = 0 range_end = MAX_FISHING_TASKS default = MAX_FISHING_TASKS @@ -309,6 +332,7 @@ class FishingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Fishing Task Weight" range_start = 0 range_end = 99 default = 50 @@ -319,6 +343,7 @@ class MaxCookingLevel(Range): The highest Cooking requirement of any task generated. If set to 0, no Cooking tasks will be generated. """ + display_name = "Max Required Cooking Level" range_start = 0 range_end = 99 default = 50 @@ -330,6 +355,7 @@ class MaxCookingTasks(Range): If set to 0, no Cooking tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Cooking Task Count" range_start = 0 range_end = MAX_COOKING_TASKS default = MAX_COOKING_TASKS @@ -341,6 +367,7 @@ class CookingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Cooking Task Weight" range_start = 0 range_end = 99 default = 50 @@ -351,6 +378,7 @@ class MaxFiremakingLevel(Range): The highest Firemaking requirement of any task generated. If set to 0, no Firemaking tasks will be generated. """ + display_name = "Max Required Firemaking Level" range_start = 0 range_end = 99 default = 50 @@ -362,6 +390,7 @@ class MaxFiremakingTasks(Range): If set to 0, no Firemaking tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Firemaking Task Count" range_start = 0 range_end = MAX_FIREMAKING_TASKS default = MAX_FIREMAKING_TASKS @@ -373,6 +402,7 @@ class FiremakingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Firemaking Task Weight" range_start = 0 range_end = 99 default = 50 @@ -383,6 +413,7 @@ class MaxWoodcuttingLevel(Range): The highest Woodcutting requirement of any task generated. If set to 0, no Woodcutting tasks will be generated. """ + display_name = "Max Required Woodcutting Level" range_start = 0 range_end = 99 default = 50 @@ -394,6 +425,7 @@ class MaxWoodcuttingTasks(Range): If set to 0, no Woodcutting tasks will be generated. This only determines the maximum possible, fewer than the maximum could be assigned. """ + display_name = "Max Woodcutting Task Count" range_start = 0 range_end = MAX_WOODCUTTING_TASKS default = MAX_WOODCUTTING_TASKS @@ -405,6 +437,7 @@ class WoodcuttingTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "Woodcutting Task Weight" range_start = 0 range_end = 99 default = 50 @@ -416,6 +449,7 @@ class MinimumGeneralTasks(Range): General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so there is no maximum. """ + display_name = "Minimum General Task Count" range_start = 0 range_end = NON_QUEST_LOCATION_COUNT default = 10 @@ -427,6 +461,7 @@ class GeneralTaskWeight(Range): Weights of all Task Types will be compared against each other, a task with 50 weight is twice as likely to appear as one with 25. """ + display_name = "General Task Weight" range_start = 0 range_end = 99 default = 50 diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 1b7ca9c1e0f4..9ed55f218d9f 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -33,6 +33,12 @@ class OSRSWeb(WebWorld): class OSRSWorld(World): + """ + The best retro fantasy MMORPG on the planet. Old School is RuneScape but… older! This is the open world you know and love, but as it was in 2007. + The Randomizer takes the form of a Chunk-Restricted f2p Ironman that takes a brand new account up through defeating + the Green Dragon of Crandor and earning a spot in the fabled Champion's Guild! + """ + game = "Old School Runescape" options_dataclass = OSRSOptions options: OSRSOptions @@ -84,16 +90,18 @@ def generate_early(self) -> None: rnd = self.random starting_area = self.options.starting_area + + #UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT + if not hasattr(self.multiworld, "generation_is_fake"): + if starting_area.value == StartingArea.option_any_bank: + self.starting_area_item = rnd.choice(starting_area_dict) + elif starting_area.value < StartingArea.option_chunksanity: + self.starting_area_item = starting_area_dict[starting_area.value] + else: + self.starting_area_item = rnd.choice(chunksanity_starting_chunks) - if starting_area.value == StartingArea.option_any_bank: - self.starting_area_item = rnd.choice(starting_area_dict) - elif starting_area.value < StartingArea.option_chunksanity: - self.starting_area_item = starting_area_dict[starting_area.value] - else: - self.starting_area_item = rnd.choice(chunksanity_starting_chunks) - - # Set Starting Chunk - self.multiworld.push_precollected(self.create_item(self.starting_area_item)) + # Set Starting Chunk + self.multiworld.push_precollected(self.create_item(self.starting_area_item)) """ This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. @@ -103,8 +111,23 @@ def generate_early(self) -> None: def fill_slot_data(self): data = self.options.as_dict("brutal_grinds") data["data_csv_tag"] = data_csv_tag + data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv return data + def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: + if "starting_area" in slot_data: + self.starting_area_item = slot_data["starting_area"] + menu_region = self.multiworld.get_region("Menu",self.player) + menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) + + def create_regions(self) -> None: """ called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done @@ -122,13 +145,14 @@ def create_regions(self) -> None: # Removes the word "Area: " from the item name to get the region it applies to. # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse - if self.starting_area_item in chunksanity_special_region_names: - starting_area_region = chunksanity_special_region_names[self.starting_area_item] - else: - starting_area_region = self.starting_area_item[6:] # len("Area: ") - starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") - starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) - starting_entrance.connect(self.region_name_to_data[starting_area_region]) + if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) # Create entrances between regions for region_row in region_rows: @@ -635,7 +659,7 @@ def can_gold(state): else: return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ (can_gold(state) and can_smelt_gold(state)) - if skill.lower() == "Cooking": + if skill.lower() == "cooking": if self.options.brutal_grinds or level < 15: return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ state.can_reach(RegionNames.Egg, "Region", self.player) or \ diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 0437c0dae8ff..6a1844e79fde 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -1,3 +1,21 @@ +# 2.3.0 + +### Features + +- Added a Swedish translation of the setup guide. +- The client communicates map transitions to any trackers connected to the slot. +- Added the player's Normalize Encounter Rates option to slot data for trackers. + +### Fixes + +- Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if +the player randomized NPC gifts. +- The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. +- A Team Magma Grunt in the Space Center which could become unreachable while trainersanity is active by overlapping +with another NPC was moved to an unoccupied space. +- Fixed a problem where the client would crash on certain operating systems while using certain python versions if the +player tried to wonder trade. + # 2.2.0 ### Features @@ -175,6 +193,7 @@ turn to face you when you run. species equally likely to appear, but makes rare encounters less rare. - Added `Trick House` location group. - Removed `Postgame Locations` location group. +- Added a Spanish translation of the setup guide. ### QoL diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index abdee26f572f..d281dde23cb0 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -711,6 +711,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "trainersanity", "modify_118", "death_link", + "normalize_encounter_rates", ) slot_data["free_fly_location_id"] = self.free_fly_location_id slot_data["hm_requirements"] = self.hm_requirements diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 7f16015a3f12..c91b7d3e26b0 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -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] @@ -132,6 +133,7 @@ class PokemonEmeraldClient(BizHawkClient): latest_wonder_trade_reply: dict wonder_trade_cooldown: int wonder_trade_cooldown_timer: int + queued_received_trade: Optional[str] death_counter: Optional[int] previous_death_link: float @@ -139,8 +141,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 = {} @@ -153,6 +154,7 @@ def __init__(self) -> None: self.previous_death_link = 0 self.ignore_next_death_link = False self.current_map = None + self.queued_received_trade = None async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: from CommonClient import logger @@ -182,9 +184,7 @@ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: 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 @@ -352,6 +352,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # Send game clear if not ctx.finished_game and game_clear: + ctx.finished_game = True await ctx.send_msgs([{ "cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL, @@ -550,22 +551,29 @@ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[st (sb1_address + 0x37CC, [1], "System Bus"), ]) elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2: - # Game is waiting on receiving a trade. See if there are any available trades that were not - # sent by this player, and if so, try to receive one. - if self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data: + # Game is waiting on receiving a trade. + if self.queued_received_trade is not None: + # Client is holding a trade, ready to write it into the game + success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [ + (sb1_address + 0x377C, json_to_pokemon_data(self.queued_received_trade), "System Bus"), + ], [guards["SAVE BLOCK 1"]]) + + # Notify the player if it was written, otherwise hold it for the next loop + if success: + logger.info("Wonder trade received!") + self.queued_received_trade = None + + elif self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data: + # See if there are any available trades that were not sent by this player. If so, try to receive one. if any(item[0] != ctx.slot for key, item in ctx.stored_data.get(f"pokemon_wonder_trades_{ctx.team}", {}).items() if key != "_lock" and orjson.loads(item[1])["species"] <= 386): - received_trade = await self.wonder_trade_receive(ctx) - if received_trade is None: + self.queued_received_trade = await self.wonder_trade_receive(ctx) + if self.queued_received_trade is None: self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown self.wonder_trade_cooldown *= 2 self.wonder_trade_cooldown += random.randrange(0, 500) else: - await bizhawk.write(ctx.bizhawk_ctx, [ - (sb1_address + 0x377C, json_to_pokemon_data(received_trade), "System Bus"), - ]) - logger.info("Wonder trade received!") self.wonder_trade_cooldown = 5000 else: diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index d89ab5febb33..432d59387391 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -276,15 +276,13 @@ def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum: return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES -@dataclass -class TrainerPokemonData: +class TrainerPokemonData(NamedTuple): species_id: int level: int moves: Optional[Tuple[int, int, int, int]] -@dataclass -class TrainerPartyData: +class TrainerPartyData(NamedTuple): pokemon: List[TrainerPokemonData] pokemon_data_type: TrainerPokemonDataTypeEnum address: int diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py index 09e947546d7c..966d19205447 100644 --- a/worlds/pokemon_emerald/opponents.py +++ b/worlds/pokemon_emerald/opponents.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Dict, List, Set -from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data +from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, data from .options import RandomizeTrainerParties from .pokemon import filter_species_by_nearby_bst from .util import int_to_bool_array @@ -111,6 +111,6 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None: hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3] ) - new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves)) + new_party.append(pokemon._replace(species_id=new_species.species_id, moves=new_moves)) - trainer.party.pokemon = new_party + trainer.party = trainer.party._replace(pokemon=new_party) diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index c60e5e9d4f14..fec1101dab0d 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -4,8 +4,7 @@ import functools from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple -from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData, - SpeciesData, data) +from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data) from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters, RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon, TmTutorCompatibility) @@ -461,7 +460,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: type_bias, normal_bias, species.types) else: new_move = 0 - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 # All moves from here onward are actual moves. @@ -473,7 +472,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: new_move = get_random_move(world.random, {move.move_id for move in new_learnset} | world.blacklisted_moves, type_bias, normal_bias, species.types) - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 species.learnset = new_learnset @@ -581,8 +580,10 @@ def randomize_starters(world: "PokemonEmeraldWorld") -> None: picked_evolution = world.random.choice(potential_evolutions) for trainer_name, starter_position, is_evolved in rival_teams[i]: + new_species_id = picked_evolution if is_evolved else starter.species_id trainer_data = world.modified_trainers[data.constants[trainer_name]] - trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id + trainer_data.party.pokemon[starter_position] = \ + trainer_data.party.pokemon[starter_position]._replace(species_id=new_species_id) def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: @@ -594,10 +595,7 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: world.random.shuffle(shuffled_species) for i, encounter in enumerate(data.legendary_encounters): - world.modified_legendary_encounters.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_legendary_encounters.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.legendary_encounters in { RandomizeLegendaryEncounters.option_match_base_stats, @@ -621,9 +619,8 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: if should_match_bst: candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats)) - world.modified_legendary_encounters.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_legendary_encounters.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) @@ -637,10 +634,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: world.modified_misc_pokemon = [] for i, encounter in enumerate(data.misc_pokemon): - world.modified_misc_pokemon.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_misc_pokemon.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.misc_pokemon in { RandomizeMiscPokemon.option_match_base_stats, @@ -672,9 +666,8 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: if len(player_filtered_candidates) > 0: candidates = player_filtered_candidates - world.modified_misc_pokemon.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_misc_pokemon.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 75d7d575846d..2c0b5021d099 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -114,6 +114,14 @@ def get_source_data(cls) -> bytes: def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None: + # TODO: Remove when the base patch is updated to include this change + # Moves an NPC to avoid overlapping people during trainersanity + patch.write_token( + APTokenTypes.WRITE, + 0x53A298 + (0x18 * 7) + 4, # Space Center 1F event address + 8th event + 4-byte offset for x coord + struct.pack(" 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 defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: "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 defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: }.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 get_location(location: str): 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 get_location(location: str): ) 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 get_location(location: str): ) 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 get_location(location: str): 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 get_location(location: str): # 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 get_location(location: str): ) 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 get_location(location: str): ) 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 get_location(location: str): # 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 get_location(location: str): # 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 get_location(location: str): ) 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 get_location(location: str): ) 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 get_location(location: str): ) 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 get_location(location: str): ) 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 get_location(location: str): # 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 get_location(location: str): ) 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 get_location(location: str): # 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 get_location(location: str): 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( diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index c1d843189820..2065507e0d59 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -3,6 +3,7 @@ import typing import threading import base64 +import random from copy import deepcopy from typing import TextIO @@ -14,7 +15,7 @@ from .items import item_table, item_groups from .locations import location_data, PokemonRBLocation from .regions import create_regions -from .options import pokemon_rb_options +from .options import PokemonRBOptions from .rom_addresses import rom_addresses from .text import encode_text from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch @@ -71,7 +72,10 @@ class PokemonRedBlueWorld(World): Elite Four to become the champion!""" # -MuffinJets#4559 game = "Pokemon Red and Blue" - option_definitions = pokemon_rb_options + + options_dataclass = PokemonRBOptions + options: PokemonRBOptions + settings: typing.ClassVar[PokemonSettings] required_client_version = (0, 4, 2) @@ -85,8 +89,8 @@ class PokemonRedBlueWorld(World): web = PokemonWebWorld() - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) self.item_pool = [] self.total_key_items = None self.fly_map = None @@ -101,11 +105,11 @@ def __init__(self, world: MultiWorld, player: int): self.learnsets = None self.trainer_name = None self.rival_name = None - self.type_chart = None self.traps = None self.trade_mons = {} self.finished_level_scaling = threading.Event() self.dexsanity_table = [] + self.trainersanity_table = [] self.local_locs = [] @classmethod @@ -113,11 +117,109 @@ def stage_assert_generate(cls, multiworld: MultiWorld): versions = set() for player in multiworld.player_ids: if multiworld.worlds[player].game == "Pokemon Red and Blue": - versions.add(multiworld.game_version[player].current_key) + versions.add(multiworld.worlds[player].options.game_version.current_key) for version in versions: if not os.path.exists(get_base_rom_path(version)): raise FileNotFoundError(get_base_rom_path(version)) + @classmethod + def stage_generate_early(cls, multiworld: MultiWorld): + + seed_groups = {} + pokemon_rb_worlds = multiworld.get_game_worlds("Pokemon Red and Blue") + + for world in pokemon_rb_worlds: + if not (world.options.type_chart_seed.value.isdigit() or world.options.type_chart_seed.value == "random"): + seed_groups[world.options.type_chart_seed.value] = seed_groups.get(world.options.type_chart_seed.value, + []) + [world] + + copy_chart_worlds = {} + + for worlds in seed_groups.values(): + chosen_world = multiworld.random.choice(worlds) + for world in worlds: + if world is not chosen_world: + copy_chart_worlds[world.player] = chosen_world + + for world in pokemon_rb_worlds: + if world.player in copy_chart_worlds: + continue + tc_random = world.random + if world.options.type_chart_seed.value.isdigit(): + tc_random = random.Random() + tc_random.seed(int(world.options.type_chart_seed.value)) + + if world.options.randomize_type_chart == "vanilla": + chart = deepcopy(poke_data.type_chart) + elif world.options.randomize_type_chart == "randomize": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + tc_random.shuffle(matchups) + immunities = world.options.immunity_matchups.value + super_effectives = world.options.super_effective_matchups.value + not_very_effectives = world.options.not_very_effective_matchups.value + normals = world.options.normal_matchups.value + while super_effectives + not_very_effectives + normals < 225 - immunities: + if super_effectives == not_very_effectives == normals == 0: + super_effectives = 225 + not_very_effectives = 225 + normals = 225 + else: + super_effectives += world.options.super_effective_matchups.value + not_very_effectives += world.options.not_very_effective_matchups.value + normals += world.options.normal_matchups.value + if super_effectives + not_very_effectives + normals > 225 - immunities: + total = super_effectives + not_very_effectives + normals + excess = total - (225 - immunities) + subtract_amounts = ( + int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), + int((excess / (super_effectives + not_very_effectives + normals)) * normals)) + super_effectives -= subtract_amounts[0] + not_very_effectives -= subtract_amounts[1] + normals -= subtract_amounts[2] + while super_effectives + not_very_effectives + normals > 225 - immunities: + r = tc_random.randint(0, 2) + if r == 0 and super_effectives: + super_effectives -= 1 + elif r == 1 and not_very_effectives: + not_very_effectives -= 1 + elif normals: + normals -= 1 + chart = [] + for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], + [0, 10, 20, 5]): + for _ in range(matchup_list): + matchup = matchups.pop() + matchup.append(matchup_value) + chart.append(matchup) + elif world.options.randomize_type_chart == "chaos": + types = poke_data.type_names.values() + matchups = [] + for type1 in types: + for type2 in types: + matchups.append([type1, type2]) + chart = [] + values = list(range(21)) + tc_random.shuffle(matchups) + tc_random.shuffle(values) + for matchup in matchups: + value = values.pop(0) + values.append(value) + matchup.append(value) + chart.append(matchup) + # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" + # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to + # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes + # to the way effectiveness messages are generated. + world.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) + + for player in copy_chart_worlds: + multiworld.worlds[player].type_chart = copy_chart_worlds[player].type_chart + def generate_early(self): def encode_name(name, t): try: @@ -126,33 +228,33 @@ def encode_name(name, t): return encode_text(name, length=8, whitespace="@", safety=True) except KeyError as e: raise KeyError(f"Invalid character(s) in {t} name for player {self.multiworld.player_name[self.player]}") from e - if self.multiworld.trainer_name[self.player] == "choose_in_game": + if self.options.trainer_name == "choose_in_game": self.trainer_name = "choose_in_game" else: - self.trainer_name = encode_name(self.multiworld.trainer_name[self.player].value, "Player") - if self.multiworld.rival_name[self.player] == "choose_in_game": + self.trainer_name = encode_name(self.options.trainer_name.value, "Player") + if self.options.rival_name == "choose_in_game": self.rival_name = "choose_in_game" else: - self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival") + self.rival_name = encode_name(self.options.rival_name.value, "Rival") - if not self.multiworld.badgesanity[self.player]: - self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"] + if not self.options.badgesanity: + self.options.non_local_items.value -= self.item_name_groups["Badges"] - if self.multiworld.key_items_only[self.player]: - self.multiworld.trainersanity[self.player] = self.multiworld.trainersanity[self.player].from_text("off") - self.multiworld.dexsanity[self.player].value = 0 - self.multiworld.randomize_hidden_items[self.player] = \ - self.multiworld.randomize_hidden_items[self.player].from_text("off") + if self.options.key_items_only: + self.options.trainersanity.value = 0 + self.options.dexsanity.value = 0 + self.options.randomize_hidden_items = \ + self.options.randomize_hidden_items.from_text("off") - if self.multiworld.badges_needed_for_hm_moves[self.player].value >= 2: + if self.options.badges_needed_for_hm_moves.value >= 2: badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"] - if self.multiworld.badges_needed_for_hm_moves[self.player].value == 3: + if self.options.badges_needed_for_hm_moves.value == 3: badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", "Soul Badge", "Volcano Badge", "Earth Badge"] - self.multiworld.random.shuffle(badges) + self.random.shuffle(badges) badges_to_add += [badges.pop(), badges.pop()] hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"] - self.multiworld.random.shuffle(hm_moves) + self.random.shuffle(hm_moves) self.extra_badges = {} for badge in badges_to_add: self.extra_badges[hm_moves.pop()] = badge @@ -160,79 +262,17 @@ def encode_name(name, t): process_move_data(self) process_pokemon_data(self) - if self.multiworld.randomize_type_chart[self.player] == "vanilla": - chart = deepcopy(poke_data.type_chart) - elif self.multiworld.randomize_type_chart[self.player] == "randomize": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - self.multiworld.random.shuffle(matchups) - immunities = self.multiworld.immunity_matchups[self.player].value - super_effectives = self.multiworld.super_effective_matchups[self.player].value - not_very_effectives = self.multiworld.not_very_effective_matchups[self.player].value - normals = self.multiworld.normal_matchups[self.player].value - while super_effectives + not_very_effectives + normals < 225 - immunities: - if super_effectives == not_very_effectives == normals == 0: - super_effectives = 225 - not_very_effectives = 225 - normals = 225 - else: - super_effectives += self.multiworld.super_effective_matchups[self.player].value - not_very_effectives += self.multiworld.not_very_effective_matchups[self.player].value - normals += self.multiworld.normal_matchups[self.player].value - if super_effectives + not_very_effectives + normals > 225 - immunities: - total = super_effectives + not_very_effectives + normals - excess = total - (225 - immunities) - subtract_amounts = ( - int((excess / (super_effectives + not_very_effectives + normals)) * super_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * not_very_effectives), - int((excess / (super_effectives + not_very_effectives + normals)) * normals)) - super_effectives -= subtract_amounts[0] - not_very_effectives -= subtract_amounts[1] - normals -= subtract_amounts[2] - while super_effectives + not_very_effectives + normals > 225 - immunities: - r = self.multiworld.random.randint(0, 2) - if r == 0 and super_effectives: - super_effectives -= 1 - elif r == 1 and not_very_effectives: - not_very_effectives -= 1 - elif normals: - normals -= 1 - chart = [] - for matchup_list, matchup_value in zip([immunities, normals, super_effectives, not_very_effectives], - [0, 10, 20, 5]): - for _ in range(matchup_list): - matchup = matchups.pop() - matchup.append(matchup_value) - chart.append(matchup) - elif self.multiworld.randomize_type_chart[self.player] == "chaos": - types = poke_data.type_names.values() - matchups = [] - for type1 in types: - for type2 in types: - matchups.append([type1, type2]) - chart = [] - values = list(range(21)) - self.multiworld.random.shuffle(matchups) - self.multiworld.random.shuffle(values) - for matchup in matchups: - value = values.pop(0) - values.append(value) - matchup.append(value) - chart.append(matchup) - # sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective" - # matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to - # damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes - # to the way effectiveness messages are generated. - self.type_chart = sorted(chart, key=lambda matchup: -matchup[2]) - self.dexsanity_table = [ - *(True for _ in range(round(self.multiworld.dexsanity[self.player].value * 1.51))), - *(False for _ in range(151 - round(self.multiworld.dexsanity[self.player].value * 1.51))) + *(True for _ in range(round(self.options.dexsanity.value))), + *(False for _ in range(151 - round(self.options.dexsanity.value))) + ] + self.random.shuffle(self.dexsanity_table) + + self.trainersanity_table = [ + *(True for _ in range(self.options.trainersanity.value)), + *(False for _ in range(317 - self.options.trainersanity.value)) ] - self.multiworld.random.shuffle(self.dexsanity_table) + self.random.shuffle(self.trainersanity_table) def create_items(self): self.multiworld.itempool += self.item_pool @@ -275,9 +315,9 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)] def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations): - if not self.multiworld.badgesanity[self.player]: + if not self.options.badgesanity: # Door Shuffle options besides Simple place badges during door shuffling - if self.multiworld.door_shuffle[self.player] in ("off", "simple"): + if self.options.door_shuffle in ("off", "simple"): badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player] for badge in badges: self.multiworld.itempool.remove(badge) @@ -297,8 +337,8 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations for mon in poke_data.pokemon_data.keys(): state.collect(self.create_item(mon), True) state.sweep_for_advancements() - self.multiworld.random.shuffle(badges) - self.multiworld.random.shuffle(badgelocs) + self.random.shuffle(badges) + self.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() # allow_partial so that unplaced badges aren't lost, for debugging purposes fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True) @@ -318,7 +358,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations raise FillError(f"Failed to place badges for player {self.player}") verify_hm_moves(self.multiworld, self, self.player) - if self.multiworld.key_items_only[self.player]: + if self.options.key_items_only: return tms = [item for item in usefulitempool + filleritempool if item.name.startswith("TM") and (item.player == @@ -340,7 +380,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations int((int(tm.name[2:4]) - 1) / 8)] & 1 << ((int(tm.name[2:4]) - 1) % 8)] if not learnable_tms: learnable_tms = tms - tm = self.multiworld.random.choice(learnable_tms) + tm = self.random.choice(learnable_tms) loc.place_locked_item(tm) fill_locations.remove(loc) @@ -370,9 +410,9 @@ def pre_fill(self) -> None: if not all_state.can_reach(location, player=self.player): evolutions_region.locations.remove(location) - if self.multiworld.old_man[self.player] == "early_parcel": + if self.options.old_man == "early_parcel": self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 - if self.multiworld.dexsanity[self.player]: + if self.options.dexsanity: for i, mon in enumerate(poke_data.pokemon_data): if self.dexsanity_table[i]: location = self.multiworld.get_location(f"Pokedex - {mon}", self.player) @@ -384,13 +424,13 @@ def pre_fill(self) -> None: locs = {self.multiworld.get_location("Fossil - Choice A", self.player), self.multiworld.get_location("Fossil - Choice B", self.player)} - if not self.multiworld.key_items_only[self.player]: + if not self.options.key_items_only: rule = None - if self.multiworld.fossil_check_item_types[self.player] == "key_items": + if self.options.fossil_check_item_types == "key_items": rule = lambda i: i.advancement - elif self.multiworld.fossil_check_item_types[self.player] == "unique_items": + elif self.options.fossil_check_item_types == "unique_items": rule = lambda i: i.name in item_groups["Unique"] - elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items": + elif self.options.fossil_check_item_types == "no_key_items": rule = lambda i: not i.advancement if rule: for loc in locs: @@ -406,16 +446,16 @@ def pre_fill(self) -> None: if loc.item is None: locs.add(loc) - if not self.multiworld.key_items_only[self.player]: + if not self.options.key_items_only: loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player) if loc.item is None: locs.add(loc) for loc in sorted(locs): - if loc.name in self.multiworld.priority_locations[self.player].value: + if loc.name in self.options.priority_locations.value: add_item_rule(loc, lambda i: i.advancement) add_item_rule(loc, lambda i: i.player == self.player) - if self.multiworld.old_man[self.player] == "early_parcel" and loc.name != "Player's House 2F - Player's PC": + if self.options.old_man == "early_parcel" and loc.name != "Player's House 2F - Player's PC": add_item_rule(loc, lambda i: i.name != "Oak's Parcel") self.local_locs = locs @@ -440,10 +480,10 @@ def pre_fill(self) -> None: else: region_mons.add(location.item.name) - self.multiworld.elite_four_pokedex_condition[self.player].total = \ - int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value) + self.options.elite_four_pokedex_condition.total = \ + int((len(reachable_mons) / 100) * self.options.elite_four_pokedex_condition.value) - if self.multiworld.accessibility[self.player] == "full": + if self.options.accessibility == "full": balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]] traps = [self.create_item(trap) for trap in item_groups["Traps"]] locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in @@ -469,7 +509,7 @@ def pre_fill(self) -> None: else: break else: - self.multiworld.random.shuffle(traps) + self.random.shuffle(traps) for trap in traps: try: self.multiworld.itempool.remove(trap) @@ -497,22 +537,22 @@ def stage_post_fill(cls, multiworld): found_mons.add(key) def create_regions(self): - if (self.multiworld.old_man[self.player] == "vanilla" or - self.multiworld.door_shuffle[self.player] in ("full", "insanity")): - fly_map_codes = self.multiworld.random.sample(range(2, 11), 2) - elif (self.multiworld.door_shuffle[self.player] == "simple" or - self.multiworld.route_3_condition[self.player] == "boulder_badge" or - (self.multiworld.route_3_condition[self.player] == "any_badge" and - self.multiworld.badgesanity[self.player])): - fly_map_codes = self.multiworld.random.sample(range(3, 11), 2) + if (self.options.old_man == "vanilla" or + self.options.door_shuffle in ("full", "insanity")): + fly_map_codes = self.random.sample(range(2, 11), 2) + elif (self.options.door_shuffle == "simple" or + self.options.route_3_condition == "boulder_badge" or + (self.options.route_3_condition == "any_badge" and + self.options.badgesanity)): + fly_map_codes = self.random.sample(range(3, 11), 2) else: - fly_map_codes = self.multiworld.random.sample([4, 6, 7, 8, 9, 10], 2) - if self.multiworld.free_fly_location[self.player]: + fly_map_codes = self.random.sample([4, 6, 7, 8, 9, 10], 2) + if self.options.free_fly_location: fly_map_code = fly_map_codes[0] else: fly_map_code = 0 - if self.multiworld.town_map_fly_location[self.player]: + if self.options.town_map_fly_location: town_map_fly_map_code = fly_map_codes[1] else: town_map_fly_map_code = 0 @@ -528,7 +568,7 @@ def create_regions(self): self.multiworld.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self.multiworld, self, self.player) def create_item(self, name: str) -> Item: return PokemonRBItem(name, self.player) @@ -548,19 +588,19 @@ def modify_multidata(self, multidata: dict): multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] def write_spoiler_header(self, spoiler_handle: TextIO): - spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n") - spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n") - spoiler_handle.write(f"Elite Four Total Pokemon: {self.multiworld.elite_four_pokedex_condition[self.player].total}\n") - if self.multiworld.free_fly_location[self.player]: + spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.options.cerulean_cave_key_items_condition.total}\n") + spoiler_handle.write(f"Elite Four Total Key Items: {self.options.elite_four_key_items_condition.total}\n") + spoiler_handle.write(f"Elite Four Total Pokemon: {self.options.elite_four_pokedex_condition.total}\n") + if self.options.free_fly_location: spoiler_handle.write(f"Free Fly Location: {self.fly_map}\n") - if self.multiworld.town_map_fly_location[self.player]: + if self.options.town_map_fly_location: spoiler_handle.write(f"Town Map Fly Location: {self.town_map_fly_map}\n") if self.extra_badges: for hm_move, badge in self.extra_badges.items(): spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n") def write_spoiler(self, spoiler_handle): - if self.multiworld.randomize_type_chart[self.player].value: + if self.options.randomize_type_chart: spoiler_handle.write(f"\n\nType matchups ({self.multiworld.player_name[self.player]}):\n\n") for matchup in self.type_chart: spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n") @@ -571,39 +611,39 @@ def write_spoiler(self, spoiler_handle): spoiler_handle.write(location.name + ": " + location.item.name + "\n") def get_filler_item_name(self) -> str: - combined_traps = (self.multiworld.poison_trap_weight[self.player].value - + self.multiworld.fire_trap_weight[self.player].value - + self.multiworld.paralyze_trap_weight[self.player].value - + self.multiworld.ice_trap_weight[self.player].value - + self.multiworld.sleep_trap_weight[self.player].value) + combined_traps = (self.options.poison_trap_weight.value + + self.options.fire_trap_weight.value + + self.options.paralyze_trap_weight.value + + self.options.ice_trap_weight.value + + self.options.sleep_trap_weight.value) if (combined_traps > 0 and - self.multiworld.random.randint(1, 100) <= self.multiworld.trap_percentage[self.player].value): + self.random.randint(1, 100) <= self.options.trap_percentage.value): return self.select_trap() banned_items = item_groups["Unique"] - if (((not self.multiworld.tea[self.player]) or "Saffron City" not in [self.fly_map, self.town_map_fly_map]) - and (not self.multiworld.door_shuffle[self.player])): + if (((not self.options.tea) or "Saffron City" not in [self.fly_map, self.town_map_fly_map]) + and (not self.options.door_shuffle)): # under these conditions, you should never be able to reach the Copycat or Pokémon Tower without being # able to reach the Celadon Department Store, so Poké Dolls would not allow early access to anything banned_items.append("Poke Doll") - if not self.multiworld.tea[self.player]: + if not self.options.tea: banned_items += item_groups["Vending Machine Drinks"] - return self.multiworld.random.choice([item for item in item_table if item_table[item].id and item_table[ + return self.random.choice([item for item in item_table if item_table[item].id and item_table[ item].classification == ItemClassification.filler and item not in banned_items]) def select_trap(self): if self.traps is None: self.traps = [] - self.traps += ["Poison Trap"] * self.multiworld.poison_trap_weight[self.player].value - self.traps += ["Fire Trap"] * self.multiworld.fire_trap_weight[self.player].value - self.traps += ["Paralyze Trap"] * self.multiworld.paralyze_trap_weight[self.player].value - self.traps += ["Ice Trap"] * self.multiworld.ice_trap_weight[self.player].value - self.traps += ["Sleep Trap"] * self.multiworld.sleep_trap_weight[self.player].value - return self.multiworld.random.choice(self.traps) + self.traps += ["Poison Trap"] * self.options.poison_trap_weight.value + self.traps += ["Fire Trap"] * self.options.fire_trap_weight.value + self.traps += ["Paralyze Trap"] * self.options.paralyze_trap_weight.value + self.traps += ["Ice Trap"] * self.options.ice_trap_weight.value + self.traps += ["Sleep Trap"] * self.options.sleep_trap_weight.value + return self.random.choice(self.traps) def extend_hint_information(self, hint_data): - if self.multiworld.dexsanity[self.player] or self.multiworld.door_shuffle[self.player]: + if self.options.dexsanity or self.options.door_shuffle: hint_data[self.player] = {} - if self.multiworld.dexsanity[self.player]: + if self.options.dexsanity: mon_locations = {mon: set() for mon in poke_data.pokemon_data.keys()} for loc in location_data: if loc.type in ["Wild Encounter", "Static Pokemon", "Legendary Pokemon", "Missable Pokemon"]: @@ -616,57 +656,59 @@ def extend_hint_information(self, hint_data): hint_data[self.player][self.multiworld.get_location(f"Pokedex - {mon}", self.player).address] =\ ", ".join(mon_locations[mon]) - if self.multiworld.door_shuffle[self.player]: + if self.options.door_shuffle: for location in self.multiworld.get_locations(self.player): if location.parent_region.entrance_hint and location.address: hint_data[self.player][location.address] = location.parent_region.entrance_hint def fill_slot_data(self) -> dict: - return { - "second_fossil_check_condition": self.multiworld.second_fossil_check_condition[self.player].value, - "require_item_finder": self.multiworld.require_item_finder[self.player].value, - "randomize_hidden_items": self.multiworld.randomize_hidden_items[self.player].value, - "badges_needed_for_hm_moves": self.multiworld.badges_needed_for_hm_moves[self.player].value, - "oaks_aide_rt_2": self.multiworld.oaks_aide_rt_2[self.player].value, - "oaks_aide_rt_11": self.multiworld.oaks_aide_rt_11[self.player].value, - "oaks_aide_rt_15": self.multiworld.oaks_aide_rt_15[self.player].value, - "extra_key_items": self.multiworld.extra_key_items[self.player].value, - "extra_strength_boulders": self.multiworld.extra_strength_boulders[self.player].value, - "tea": self.multiworld.tea[self.player].value, - "old_man": self.multiworld.old_man[self.player].value, - "elite_four_badges_condition": self.multiworld.elite_four_badges_condition[self.player].value, - "elite_four_key_items_condition": self.multiworld.elite_four_key_items_condition[self.player].total, - "elite_four_pokedex_condition": self.multiworld.elite_four_pokedex_condition[self.player].total, - "victory_road_condition": self.multiworld.victory_road_condition[self.player].value, - "route_22_gate_condition": self.multiworld.route_22_gate_condition[self.player].value, - "route_3_condition": self.multiworld.route_3_condition[self.player].value, - "robbed_house_officer": self.multiworld.robbed_house_officer[self.player].value, - "viridian_gym_condition": self.multiworld.viridian_gym_condition[self.player].value, - "cerulean_cave_badges_condition": self.multiworld.cerulean_cave_badges_condition[self.player].value, - "cerulean_cave_key_items_condition": self.multiworld.cerulean_cave_key_items_condition[self.player].total, + ret = { + "second_fossil_check_condition": self.options.second_fossil_check_condition.value, + "require_item_finder": self.options.require_item_finder.value, + "randomize_hidden_items": self.options.randomize_hidden_items.value, + "badges_needed_for_hm_moves": self.options.badges_needed_for_hm_moves.value, + "oaks_aide_rt_2": self.options.oaks_aide_rt_2.value, + "oaks_aide_rt_11": self.options.oaks_aide_rt_11.value, + "oaks_aide_rt_15": self.options.oaks_aide_rt_15.value, + "extra_key_items": self.options.extra_key_items.value, + "extra_strength_boulders": self.options.extra_strength_boulders.value, + "tea": self.options.tea.value, + "old_man": self.options.old_man.value, + "elite_four_badges_condition": self.options.elite_four_badges_condition.value, + "elite_four_key_items_condition": self.options.elite_four_key_items_condition.total, + "elite_four_pokedex_condition": self.options.elite_four_pokedex_condition.total, + "victory_road_condition": self.options.victory_road_condition.value, + "route_22_gate_condition": self.options.route_22_gate_condition.value, + "route_3_condition": self.options.route_3_condition.value, + "robbed_house_officer": self.options.robbed_house_officer.value, + "viridian_gym_condition": self.options.viridian_gym_condition.value, + "cerulean_cave_badges_condition": self.options.cerulean_cave_badges_condition.value, + "cerulean_cave_key_items_condition": self.options.cerulean_cave_key_items_condition.total, "free_fly_map": self.fly_map_code, "town_map_fly_map": self.town_map_fly_map_code, "extra_badges": self.extra_badges, - "type_chart": self.type_chart, - "randomize_pokedex": self.multiworld.randomize_pokedex[self.player].value, - "trainersanity": self.multiworld.trainersanity[self.player].value, - "death_link": self.multiworld.death_link[self.player].value, - "prizesanity": self.multiworld.prizesanity[self.player].value, - "key_items_only": self.multiworld.key_items_only[self.player].value, - "poke_doll_skip": self.multiworld.poke_doll_skip[self.player].value, - "bicycle_gate_skips": self.multiworld.bicycle_gate_skips[self.player].value, - "stonesanity": self.multiworld.stonesanity[self.player].value, - "door_shuffle": self.multiworld.door_shuffle[self.player].value, - "warp_tile_shuffle": self.multiworld.warp_tile_shuffle[self.player].value, - "dark_rock_tunnel_logic": self.multiworld.dark_rock_tunnel_logic[self.player].value, - "split_card_key": self.multiworld.split_card_key[self.player].value, - "all_elevators_locked": self.multiworld.all_elevators_locked[self.player].value, - "require_pokedex": self.multiworld.require_pokedex[self.player].value, - "area_1_to_1_mapping": self.multiworld.area_1_to_1_mapping[self.player].value, - "blind_trainers": self.multiworld.blind_trainers[self.player].value, + "randomize_pokedex": self.options.randomize_pokedex.value, + "trainersanity": self.options.trainersanity.value, + "death_link": self.options.death_link.value, + "prizesanity": self.options.prizesanity.value, + "key_items_only": self.options.key_items_only.value, + "poke_doll_skip": self.options.poke_doll_skip.value, + "bicycle_gate_skips": self.options.bicycle_gate_skips.value, + "stonesanity": self.options.stonesanity.value, + "door_shuffle": self.options.door_shuffle.value, + "warp_tile_shuffle": self.options.warp_tile_shuffle.value, + "dark_rock_tunnel_logic": self.options.dark_rock_tunnel_logic.value, + "split_card_key": self.options.split_card_key.value, + "all_elevators_locked": self.options.all_elevators_locked.value, + "require_pokedex": self.options.require_pokedex.value, + "area_1_to_1_mapping": self.options.area_1_to_1_mapping.value, + "blind_trainers": self.options.blind_trainers.value, } + if self.options.type_chart_seed == "random" or self.options.type_chart_seed.value.isdigit(): + ret["type_chart"] = self.type_chart + return ret class PokemonRBItem(Item): game = "Pokemon Red and Blue" diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index 0f65564a737b..bcd94c632d2c 100644 Binary files a/worlds/pokemon_rb/basepatch_blue.bsdiff4 and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index 826b7bf8b4e5..4b207108cf0c 100644 Binary files a/worlds/pokemon_rb/basepatch_red.bsdiff4 and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index 1e5c14eb99f5..6811b5926078 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -60,11 +60,12 @@ and Safari Zone. Adds 4 extra item locations to Rock Tunnel B1F * Split Card Key: Splits the Card Key into 10 different Card Keys, one for each floor of Silph Co that has locked doors. Adds 9 location checks to friendly NPCs in Silph Co. You can also choose Progressive Card Keys to always obtain the keys in order from Card Key 2F to Card Key 11F. -* Trainersanity: Adds location checks to 317 trainers. Does not include scripted trainers, most of which disappear +* Trainersanity: Adds location checks to trainers. You may choose between 0 and 317 trainersanity checks. Trainers +will be randomly selected to be given checks. Does not include scripted trainers, most of which disappear after battling them, but also includes Gym Leaders. You must talk to the trainer after defeating them to receive -your prize. Adds 317 random filler items to the item pool -* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose a percentage -of Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty +your prize. Adds random filler items to the item pool. +* Dexsanity: Location checks occur when registering Pokémon as owned in the Pokédex. You can choose between 0 and 151 +Pokémon to have checks added to, chosen randomly. You can identify which Pokémon have location checks by an empty Poké Ball icon shown in battle or in the Pokédex menu. ## Which items can be in another player's world? diff --git a/worlds/pokemon_rb/encounters.py b/worlds/pokemon_rb/encounters.py index 6d1762b0ca71..fbe4abfe4466 100644 --- a/worlds/pokemon_rb/encounters.py +++ b/worlds/pokemon_rb/encounters.py @@ -8,7 +8,7 @@ def get_encounter_slots(self): for location in encounter_slots: if isinstance(location.original_item, list): - location.original_item = location.original_item[not self.multiworld.game_version[self.player].value] + location.original_item = location.original_item[not self.options.game_version.value] return encounter_slots @@ -39,16 +39,16 @@ def randomize_pokemon(self, mon, mons_list, randomize_type, random): return mon -def process_trainer_data(self): +def process_trainer_data(world): mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon - or self.multiworld.trainer_legendaries[self.player].value] + or world.options.trainer_legendaries.value] unevolved_mons = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] + or world.options.randomize_legendary_pokemon.value == 3] evolved_mons = [mon for mon in mons_list if mon not in unevolved_mons] rival_map = { - "Charmander": self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name[9:], # strip the - "Squirtle": self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name[9:], # 'Missable' - "Bulbasaur": self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name[9:], # from the name + "Charmander": world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name[9:], # strip the + "Squirtle": world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name[9:], # 'Missable' + "Bulbasaur": world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name[9:], # from the name } def add_evolutions(): @@ -60,7 +60,7 @@ def add_evolutions(): rival_map[poke_data.evolves_to[a]] = b add_evolutions() add_evolutions() - parties_objs = [location for location in self.multiworld.get_locations(self.player) + parties_objs = [location for location in world.multiworld.get_locations(world.player) if location.type == "Trainer Parties"] # Process Rival parties in order "Route 22 " is not a typo parties_objs.sort(key=lambda i: 0 if "Oak's Lab" in i.name else 1 if "Route 22 " in i.name else 2 if "Cerulean City" @@ -75,25 +75,25 @@ def add_evolutions(): for i, mon in enumerate(rival_party): if mon in ("Bulbasaur", "Ivysaur", "Venusaur", "Charmander", "Charmeleon", "Charizard", "Squirtle", "Wartortle", "Blastoise"): - if self.multiworld.randomize_starter_pokemon[self.player]: + if world.options.randomize_starter_pokemon: rival_party[i] = rival_map[mon] - elif self.multiworld.randomize_trainer_parties[self.player]: + elif world.options.randomize_trainer_parties: if mon in rival_map: rival_party[i] = rival_map[mon] else: - new_mon = randomize_pokemon(self, mon, + new_mon = randomize_pokemon(world, mon, unevolved_mons if mon in unevolved_mons else evolved_mons, - self.multiworld.randomize_trainer_parties[self.player].value, - self.multiworld.random) + world.options.randomize_trainer_parties.value, + world.random) rival_map[mon] = new_mon rival_party[i] = new_mon add_evolutions() else: - if self.multiworld.randomize_trainer_parties[self.player]: + if world.options.randomize_trainer_parties: for i, mon in enumerate(party["party"]): - party["party"][i] = randomize_pokemon(self, mon, mons_list, - self.multiworld.randomize_trainer_parties[self.player].value, - self.multiworld.random) + party["party"][i] = randomize_pokemon(world, mon, mons_list, + world.options.randomize_trainer_parties.value, + world.random) def process_pokemon_locations(self): @@ -106,21 +106,21 @@ def process_pokemon_locations(self): placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()} mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] - if self.multiworld.randomize_legendary_pokemon[self.player] == "vanilla": + or self.options.randomize_legendary_pokemon.value == 3] + if self.options.randomize_legendary_pokemon == "vanilla": for slot in legendary_slots: location = self.multiworld.get_location(slot.name, self.player) location.place_locked_item(self.create_item("Static " + slot.original_item)) - elif self.multiworld.randomize_legendary_pokemon[self.player] == "shuffle": - self.multiworld.random.shuffle(legendary_mons) + elif self.options.randomize_legendary_pokemon == "shuffle": + self.random.shuffle(legendary_mons) for slot in legendary_slots: location = self.multiworld.get_location(slot.name, self.player) mon = legendary_mons.pop() location.place_locked_item(self.create_item("Static " + mon)) placed_mons[mon] += 1 - elif self.multiworld.randomize_legendary_pokemon[self.player] == "static": + elif self.options.randomize_legendary_pokemon == "static": static_slots = static_slots + legendary_slots - self.multiworld.random.shuffle(static_slots) + self.random.shuffle(static_slots) static_slots.sort(key=lambda s: s.name != "Pokemon Tower 6F - Restless Soul") while legendary_slots: swap_slot = legendary_slots.pop() @@ -131,12 +131,12 @@ def process_pokemon_locations(self): location = self.multiworld.get_location(slot.name, self.player) location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item)) swap_slot.original_item = slot.original_item - elif self.multiworld.randomize_legendary_pokemon[self.player] == "any": + elif self.options.randomize_legendary_pokemon == "any": static_slots = static_slots + legendary_slots for slot in static_slots: location = self.multiworld.get_location(slot.name, self.player) - randomize_type = self.multiworld.randomize_static_pokemon[self.player].value + randomize_type = self.options.randomize_static_pokemon.value slot_type = slot.type.split()[0] if slot_type == "Legendary": slot_type = "Static" @@ -145,7 +145,7 @@ def process_pokemon_locations(self): else: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, randomize_type, - self.multiworld.random)) + self.random)) location.place_locked_item(mon) if slot_type != "Missable": placed_mons[mon.name.replace("Static ", "")] += 1 @@ -153,16 +153,16 @@ def process_pokemon_locations(self): chosen_mons = set() for slot in starter_slots: location = self.multiworld.get_location(slot.name, self.player) - randomize_type = self.multiworld.randomize_starter_pokemon[self.player].value + randomize_type = self.options.randomize_starter_pokemon.value slot_type = "Missable" if not randomize_type: location.place_locked_item(self.create_item(slot_type + " " + slot.original_item)) else: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, - randomize_type, self.multiworld.random)) + randomize_type, self.random)) while mon.name in chosen_mons: mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list, - randomize_type, self.multiworld.random)) + randomize_type, self.random)) chosen_mons.add(mon.name) location.place_locked_item(mon) @@ -170,10 +170,10 @@ def process_pokemon_locations(self): encounter_slots = encounter_slots_master.copy() zone_mapping = {} - if self.multiworld.randomize_wild_pokemon[self.player]: + if self.options.randomize_wild_pokemon: mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon - or self.multiworld.randomize_legendary_pokemon[self.player].value == 3] - self.multiworld.random.shuffle(encounter_slots) + or self.options.randomize_legendary_pokemon.value == 3] + self.random.shuffle(encounter_slots) locations = [] for slot in encounter_slots: location = self.multiworld.get_location(slot.name, self.player) @@ -181,11 +181,11 @@ def process_pokemon_locations(self): if zone not in zone_mapping: zone_mapping[zone] = {} original_mon = slot.original_item - if self.multiworld.area_1_to_1_mapping[self.player] and original_mon in zone_mapping[zone]: + if self.options.area_1_to_1_mapping and original_mon in zone_mapping[zone]: mon = zone_mapping[zone][original_mon] else: mon = randomize_pokemon(self, original_mon, mons_list, - self.multiworld.randomize_wild_pokemon[self.player].value, self.multiworld.random) + self.options.randomize_wild_pokemon.value, self.random) # while ("Pokemon Tower 6F" in slot.name and self.multiworld.get_location("Pokemon Tower 6F - Restless Soul", self.player).item.name @@ -194,7 +194,7 @@ def process_pokemon_locations(self): # the battle is treates as the Restless Soul battle and you cannot catch it. So, prevent any wild mons # from being the same species as the Restless Soul. # to account for the possibility that only one ground type Pokemon exists, match only stats for this fix - mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random) + mon = randomize_pokemon(self, original_mon, mons_list, 2, self.random) placed_mons[mon] += 1 location.item = self.create_item(mon) location.locked = True @@ -204,28 +204,28 @@ def process_pokemon_locations(self): mons_to_add = [] remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and - (pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)] - if self.multiworld.catch_em_all[self.player] == "first_stage": + (pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)] + if self.options.catch_em_all == "first_stage": mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and - (pokemon not in poke_data.legendary_pokemon or self.multiworld.randomize_legendary_pokemon[self.player].value == 3)] - elif self.multiworld.catch_em_all[self.player] == "all_pokemon": + (pokemon not in poke_data.legendary_pokemon or self.options.randomize_legendary_pokemon.value == 3)] + elif self.options.catch_em_all == "all_pokemon": mons_to_add = remaining_pokemon.copy() - logic_needed_mons = max(self.multiworld.oaks_aide_rt_2[self.player].value, - self.multiworld.oaks_aide_rt_11[self.player].value, - self.multiworld.oaks_aide_rt_15[self.player].value) - if self.multiworld.accessibility[self.player] == "minimal": + logic_needed_mons = max(self.options.oaks_aide_rt_2.value, + self.options.oaks_aide_rt_11.value, + self.options.oaks_aide_rt_15.value) + if self.options.accessibility == "minimal": logic_needed_mons = 0 - self.multiworld.random.shuffle(remaining_pokemon) + self.random.shuffle(remaining_pokemon) while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0]) + len(mons_to_add) < logic_needed_mons): mons_to_add.append(remaining_pokemon.pop()) for mon in mons_to_add: stat_base = get_base_stat_total(mon) candidate_locations = encounter_slots_master.copy() - if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_base_stats", "match_types_and_base_stats"]: + if self.options.randomize_wild_pokemon.current_key in ["match_base_stats", "match_types_and_base_stats"]: candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.original_item) - stat_base)) - if self.multiworld.randomize_wild_pokemon[self.player].current_key in ["match_types", "match_types_and_base_stats"]: + if self.options.randomize_wild_pokemon.current_key in ["match_types", "match_types_and_base_stats"]: candidate_locations.sort(key=lambda slot: not any([poke_data.pokemon_data[slot.original_item]["type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]], poke_data.pokemon_data[slot.original_item]["type2"] in @@ -233,12 +233,12 @@ def process_pokemon_locations(self): candidate_locations = [self.multiworld.get_location(location.name, self.player) for location in candidate_locations] for location in candidate_locations: zone = " - ".join(location.name.split(" - ")[:-1]) - if self.multiworld.catch_em_all[self.player] == "all_pokemon" and self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.catch_em_all == "all_pokemon" and self.options.area_1_to_1_mapping: if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master if (not l.name.startswith(zone)) and self.multiworld.get_location(l.name, self.player).item.name == location.item.name]: continue - if self.multiworld.catch_em_all[self.player] == "first_stage" and self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.catch_em_all == "first_stage" and self.options.area_1_to_1_mapping: if not [self.multiworld.get_location(l.name, self.player) for l in encounter_slots_master if (not l.name.startswith(zone)) and self.multiworld.get_location(l.name, self.player).item.name == location.item.name and l.name @@ -246,10 +246,10 @@ def process_pokemon_locations(self): continue if placed_mons[location.item.name] < 2 and (location.item.name in poke_data.first_stage_pokemon - or self.multiworld.catch_em_all[self.player]): + or self.options.catch_em_all): continue - if self.multiworld.area_1_to_1_mapping[self.player]: + if self.options.area_1_to_1_mapping: place_locations = [place_location for place_location in candidate_locations if place_location.name.startswith(zone) and place_location.item.name == location.item.name] diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index de29f341c6df..fb439c4f80fa 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -194,6 +194,8 @@ def __init__(self, item_id, classification, groups): "Fuji Saved": ItemData(None, ItemClassification.progression, []), "Silph Co Liberated": ItemData(None, ItemClassification.progression, []), "Become Champion": ItemData(None, ItemClassification.progression, []), + "Mt Moon Fossils": ItemData(None, ItemClassification.progression, []), + "Cinnabar Lab": ItemData(None, ItemClassification.progression, []), "Trainer Parties": ItemData(None, ItemClassification.filler, []) } diff --git a/worlds/pokemon_rb/level_scaling.py b/worlds/pokemon_rb/level_scaling.py index 79cda394724a..76e00d9847c4 100644 --- a/worlds/pokemon_rb/level_scaling.py +++ b/worlds/pokemon_rb/level_scaling.py @@ -10,9 +10,9 @@ def level_scaling(multiworld): while locations: sphere = set() for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - if (multiworld.level_scaling[world.player] != "by_spheres_and_distance" - and (multiworld.level_scaling[world.player] != "auto" or multiworld.door_shuffle[world.player] - in ("off", "simple"))): + if (world.options.level_scaling != "by_spheres_and_distance" + and (world.options.level_scaling != "auto" + or world.options.door_shuffle in ("off", "simple"))): continue regions = {multiworld.get_region("Menu", world.player)} checked_regions = set() @@ -41,7 +41,8 @@ def reachable(): # reach them earlier. We treat them both as reachable right away for this purpose return True if (location.name == "Route 25 - Item" and state.can_reach("Route 25", "Region", location.player) - and multiworld.blind_trainers[location.player].value < 100): + and multiworld.worlds[location.player].options.blind_trainers.value < 100 + and "Route 25 - Jr. Trainer M" not in multiworld.regions.location_cache[location.player]): # Assume they will take their one chance to get the trainer to walk out of the way to reach # the item behind them return True @@ -95,9 +96,9 @@ def reachable(): if (location.item.game == "Pokemon Red and Blue" and (location.item.name.startswith("Missable ") or location.item.name.startswith("Static ")) and location.name != "Pokemon Tower 6F - Restless Soul"): - # Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic static Pokemon - # are not considered for moves or evolutions, as you could release them and potentially soft lock - # the game. However, for level scaling purposes, we will treat them as not missable or static. + # Normally, missable Pokemon (starters, the dojo rewards) are not considered in logic, and static + # Pokemon are not considered for moves or evolutions, as you could release them and potentially soft + # lock the game. However, for level scaling purposes, we will treat them as not missable or static. # We would not want someone playing a minimal accessibility Dexsanity game to get what would be # technically an "out of logic" Mansion Key from selecting Bulbasaur at the beginning of the game # and end up in the Mansion early and encountering level 67 Pokémon @@ -106,7 +107,7 @@ def reachable(): else: state.collect(location.item, True, location) for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - if multiworld.level_scaling[world.player] == "off": + if world.options.level_scaling == "off": continue level_list_copy = level_list.copy() for sphere in spheres: @@ -136,4 +137,4 @@ def reachable(): else: sphere_objects[object].level = level_list_copy.pop(0) for world in multiworld.get_game_worlds("Pokemon Red and Blue"): - world.finished_level_scaling.set() + world.finished_level_scaling.set() \ No newline at end of file diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 6aee25df2637..5885183baa9c 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -5,46 +5,48 @@ loc_id_start = 172000000 -def trainersanity(multiworld, player): - return multiworld.trainersanity[player] +def trainersanity(world, player): + include = world.trainersanity_table.pop(0) + world.trainersanity_table.append(include) + return include -def dexsanity(multiworld, player): - include = multiworld.worlds[player].dexsanity_table.pop(0) - multiworld.worlds[player].dexsanity_table.append(include) +def dexsanity(world, player): + include = world.dexsanity_table.pop(0) + world.dexsanity_table.append(include) return include -def hidden_items(multiworld, player): - return multiworld.randomize_hidden_items[player] +def hidden_items(world, player): + return world.options.randomize_hidden_items -def hidden_moon_stones(multiworld, player): - return multiworld.randomize_hidden_items[player] or multiworld.stonesanity[player] +def hidden_moon_stones(world, player): + return world.options.randomize_hidden_items or world.options.stonesanity -def tea(multiworld, player): - return multiworld.tea[player] +def tea(world, player): + return world.options.tea -def extra_key_items(multiworld, player): - return multiworld.extra_key_items[player] +def extra_key_items(world, player): + return world.options.extra_key_items -def always_on(multiworld, player): +def always_on(world, player): return True -def prizesanity(multiworld, player): - return multiworld.prizesanity[player] +def prizesanity(world, player): + return world.options.prizesanity -def split_card_key(multiworld, player): - return multiworld.split_card_key[player].value > 0 +def split_card_key(world, player): + return world.options.split_card_key.value > 0 -def not_stonesanity(multiworld, player): - return not multiworld.stonesanity[player] +def not_stonesanity(world, player): + return not world.options.stonesanity class LocationData: @@ -395,7 +397,7 @@ def __init__(self, flag): LocationData("Silph Co 5F", "Hidden Item Pot Plant", "Elixir", rom_addresses['Hidden_Item_Silph_Co_5F'], Hidden(18), inclusion=hidden_items), LocationData("Silph Co 9F-SW", "Hidden Item Nurse Bed", "Max Potion", rom_addresses['Hidden_Item_Silph_Co_9F'], Hidden(19), inclusion=hidden_items), LocationData("Saffron Copycat's House 2F", "Hidden Item Desk", "Nugget", rom_addresses['Hidden_Item_Copycats_House'], Hidden(20), inclusion=hidden_items), - LocationData("Cerulean Cave 1F-NW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items), + LocationData("Cerulean Cave 1F-SW", "Hidden Item Center Rocks", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_Cave_1F'], Hidden(21), inclusion=hidden_items), LocationData("Cerulean Cave B1F-E", "Hidden Item Northeast Rocks", "Ultra Ball", rom_addresses['Hidden_Item_Cerulean_Cave_B1F'], Hidden(22), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Central Dead End", "Max Elixir", rom_addresses['Hidden_Item_Power_Plant_1'], Hidden(23), inclusion=hidden_items), LocationData("Power Plant", "Hidden Item Before Zapdos", "PP Up", rom_addresses['Hidden_Item_Power_Plant_2'], Hidden(24), inclusion=hidden_items), @@ -786,6 +788,8 @@ def __init__(self, flag): LocationData("Celadon Game Corner", "", "Game Corner", event=True), LocationData("Cinnabar Island", "", "Cinnabar Island", event=True), + LocationData("Cinnabar Lab", "", "Cinnabar Lab", event=True), + LocationData("Mt Moon B2F", "Mt Moon Fossils", "Mt Moon Fossils", event=True), LocationData("Celadon Department Store 4F", "Buy Poke Doll", "Buy Poke Doll", event=True), LocationData("Celadon Department Store 4F", "Buy Fire Stone", "Fire Stone", event=True, inclusion=not_stonesanity), LocationData("Celadon Department Store 4F", "Buy Water Stone", "Water Stone", event=True, inclusion=not_stonesanity), diff --git a/worlds/pokemon_rb/logic.py b/worlds/pokemon_rb/logic.py index cbe28e0ddb47..03e3fa3dfad0 100644 --- a/worlds/pokemon_rb/logic.py +++ b/worlds/pokemon_rb/logic.py @@ -1,49 +1,47 @@ from . import poke_data -def can_surf(state, player): - return (((state.has("HM03 Surf", player) and can_learn_hm(state, "Surf", player)) - or state.has("Flippers", player)) and (state.has("Soul Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Surf"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_surf(state, world, player): + return (((state.has("HM03 Surf", player) and can_learn_hm(state, world, "Surf", player))) and (state.has("Soul Badge", player) or + state.has(world.extra_badges.get("Surf"), player) + or world.options.badges_needed_for_hm_moves.value == 0)) -def can_cut(state, player): - return ((state.has("HM01 Cut", player) and can_learn_hm(state, "Cut", player) or state.has("Master Sword", player)) - and (state.has("Cascade Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Cut"), player) or - state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_cut(state, world, player): + return ((state.has("HM01 Cut", player) and can_learn_hm(state, world, "Cut", player)) + and (state.has("Cascade Badge", player) or state.has(world.extra_badges.get("Cut"), player) or + world.options.badges_needed_for_hm_moves.value == 0)) -def can_fly(state, player): - return (((state.has("HM02 Fly", player) and can_learn_hm(state, "Fly", player)) or state.has("Flute", player)) and - (state.has("Thunder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Fly"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_fly(state, world, player): + return (((state.has("HM02 Fly", player) and can_learn_hm(state, world, "Fly", player)) or state.has("Flute", player)) and + (state.has("Thunder Badge", player) or state.has(world.extra_badges.get("Fly"), player) + or world.options.badges_needed_for_hm_moves.value == 0)) -def can_strength(state, player): - return ((state.has("HM04 Strength", player) and can_learn_hm(state, "Strength", player)) or +def can_strength(state, world, player): + return ((state.has("HM04 Strength", player) and can_learn_hm(state, world, "Strength", player)) or state.has("Titan's Mitt", player)) and (state.has("Rainbow Badge", player) or - state.has(state.multiworld.worlds[player].extra_badges.get("Strength"), player) - or state.multiworld.badges_needed_for_hm_moves[player].value == 0) + state.has(world.extra_badges.get("Strength"), player) + or world.options.badges_needed_for_hm_moves.value == 0) -def can_flash(state, player): - return (((state.has("HM05 Flash", player) and can_learn_hm(state, "Flash", player)) or state.has("Lamp", player)) - and (state.has("Boulder Badge", player) or state.has(state.multiworld.worlds[player].extra_badges.get("Flash"), - player) or state.multiworld.badges_needed_for_hm_moves[player].value == 0)) +def can_flash(state, world, player): + return (((state.has("HM05 Flash", player) and can_learn_hm(state, world, "Flash", player)) or state.has("Lamp", player)) + and (state.has("Boulder Badge", player) or state.has(world.extra_badges.get("Flash"), + player) or world.options.badges_needed_for_hm_moves.value == 0)) -def can_learn_hm(state, move, player): - for pokemon, data in state.multiworld.worlds[player].local_poke_data.items(): +def can_learn_hm(state, world, move, player): + for pokemon, data in world.local_poke_data.items(): if state.has(pokemon, player) and data["tms"][6] & 1 << (["Cut", "Fly", "Surf", "Strength", "Flash"].index(move) + 2): return True return False -def can_get_hidden_items(state, player): - return state.has("Item Finder", player) or not state.multiworld.require_item_finder[player].value +def can_get_hidden_items(state, world, player): + return state.has("Item Finder", player) or not world.options.require_item_finder.value def has_key_items(state, count, player): @@ -53,13 +51,14 @@ def has_key_items(state, count, player): "Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F", "Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F", "Card Key 11F", "Exp. All", "Fire Stone", "Thunder Stone", "Water Stone", - "Leaf Stone", "Moon Stone"] if state.has(item, player)]) + "Leaf Stone", "Moon Stone", "Oak's Parcel", "Helix Fossil", "Dome Fossil", + "Old Amber", "Tea", "Gold Teeth", "Bike Voucher"] if state.has(item, player)]) + min(state.count("Progressive Card Key", player), 10)) return key_items >= count -def can_pass_guards(state, player): - if state.multiworld.tea[player]: +def can_pass_guards(state, world, player): + if world.options.tea: return state.has("Tea", player) else: return state.has("Vending Machine Drinks", player) @@ -70,8 +69,8 @@ def has_badges(state, count, player): "Soul Badge", "Volcano Badge", "Earth Badge"] if state.has(item, player)]) >= count -def oaks_aide(state, count, player): - return ((not state.multiworld.require_pokedex[player] or state.has("Pokedex", player)) +def oaks_aide(state, world, count, player): + return ((not world.options.require_pokedex or state.has("Pokedex", player)) and has_pokemon(state, count, player)) @@ -85,9 +84,7 @@ def has_pokemon(state, count, player): def fossil_checks(state, count, player): - return (state.can_reach('Mt Moon B2F', 'Region', player) and - state.can_reach('Cinnabar Lab Fossil Room', 'Region', player) and - state.can_reach('Cinnabar Island', 'Region', player) and len( + return (state.has_all(["Mt Moon Fossils", "Cinnabar Lab", "Cinnabar Island"], player) and len( [item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if state.has(item, player)]) >= count) @@ -96,19 +93,19 @@ def card_key(state, floor, player): state.has("Progressive Card Key", player, floor - 1) -def rock_tunnel(state, player): - return can_flash(state, player) or not state.multiworld.dark_rock_tunnel_logic[player] +def rock_tunnel(state, world, player): + return can_flash(state, world, player) or not world.options.dark_rock_tunnel_logic -def route_3(state, player): - if state.multiworld.route_3_condition[player] == "defeat_brock": +def route(state, world, player): + if world.options.route_3_condition == "defeat_brock": return state.has("Defeat Brock", player) - elif state.multiworld.route_3_condition[player] == "defeat_any_gym": + elif world.options.route_3_condition == "defeat_any_gym": return state.has_any(["Defeat Brock", "Defeat Misty", "Defeat Lt. Surge", "Defeat Erika", "Defeat Koga", "Defeat Blaine", "Defeat Sabrina", "Defeat Viridian Gym Giovanni"], player) - elif state.multiworld.route_3_condition[player] == "boulder_badge": + elif world.options.route_3_condition == "boulder_badge": return state.has("Boulder Badge", player) - elif state.multiworld.route_3_condition[player] == "any_badge": + elif world.options.route_3_condition == "any_badge": return state.has_any(["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge", "Soul Badge", "Volcano Badge", "Earth Badge"], player) # open diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 9f217e82e646..21679bec00e9 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -1,4 +1,6 @@ -from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink, ItemsAccessibility +from dataclasses import dataclass +from Options import (PerGameCommonOptions, Toggle, Choice, Range, NamedRange, FreeText, TextChoice, DeathLink, + ItemsAccessibility) class GameVersion(Choice): @@ -263,12 +265,18 @@ class PrizeSanity(Toggle): default = 0 -class TrainerSanity(Toggle): - """Add a location check to every trainer in the game, which can be obtained by talking to a trainer after defeating - them. Does not affect gym leaders and some scripted event battles (including all Rival, Giovanni, and - Cinnabar Gym battles).""" +class TrainerSanity(NamedRange): + """Add location checks to trainers, which can be obtained by talking to a trainer after defeating them. Does not + affect gym leaders and some scripted event battles. You may specify a number of trainers to have checks, and in + this case they will be randomly selected. There is no in-game indication as to which trainers have checks.""" display_name = "Trainersanity" default = 0 + range_start = 0 + range_end = 317 + special_range_names = { + "disabled": 0, + "full": 317 + } class RequirePokedex(Toggle): @@ -286,19 +294,19 @@ class AllPokemonSeen(Toggle): class DexSanity(NamedRange): - """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to - have checks added. If Accessibility is set to full, this will be the percentage of all logically reachable - Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage - of all 151 Pokemon. - If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to - Professor Oak or evaluating the Pokedex via Oak's PC.""" + """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify the exact number of Dexsanity + checks to add, and they will be distributed to Pokemon randomly. + If Accessibility is set to Full, Dexsanity checks for Pokemon that are not logically reachable will be removed, + so the number could be lower than you specified. + If Pokedex is required, the Dexsanity checks for Pokemon you acquired before acquiring the Pokedex can be found by + talking to Professor Oak or evaluating the Pokedex via Oak's PC.""" display_name = "Dexsanity" default = 0 range_start = 0 - range_end = 100 + range_end = 151 special_range_names = { "disabled": 0, - "full": 100 + "full": 151 } @@ -519,7 +527,8 @@ class TrainerLegendaries(Toggle): class BlindTrainers(Range): """Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a - battle. If you move into and out of their line of sight without stopping, this chance will only trigger once.""" + battle. If you move into and out of their line of sight without stopping, this chance will only trigger once. + Trainers which have Trainersanity location checks ignore the Blind Trainers setting.""" display_name = "Blind Trainers" range_start = 0 range_end = 100 @@ -704,6 +713,15 @@ class RandomizeTypeChart(Choice): default = 0 +class TypeChartSeed(FreeText): + """You can enter a number to use as a seed for the type chart. If you enter anything besides a number or "random", + it will be used as a type chart group name, and everyone using the same group name will get the same type chart, + made using the type chart options of one random player within the group. If a group name is used, the type matchup + information will not be made available for trackers.""" + display_name = "Type Chart Seed" + default = "random" + + class NormalMatchups(Range): """If 'randomize' is chosen for Randomize Type Chart, this will be the weight for neutral matchups. No effect if 'chaos' is chosen""" @@ -850,8 +868,8 @@ class BicycleGateSkips(Choice): class RandomizePokemonPalettes(Choice): - """Modify palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, Follow - Evolutions will randomize palettes but palettes will remain the same through evolutions (except Eeveelutions), + """Modify Super Gameboy palettes of Pokemon. Primary Type will set Pokemons' palettes based on their primary type, + Follow Evolutions will randomize palettes but they will remain the same through evolutions (except Eeveelutions), Completely Random will randomize all Pokemons' palettes individually""" display_name = "Randomize Pokemon Palettes" option_vanilla = 0 @@ -860,104 +878,105 @@ class RandomizePokemonPalettes(Choice): option_completely_random = 3 -pokemon_rb_options = { - "accessibility": ItemsAccessibility, - "game_version": GameVersion, - "trainer_name": TrainerName, - "rival_name": RivalName, - #"goal": Goal, - "elite_four_badges_condition": EliteFourBadgesCondition, - "elite_four_key_items_condition": EliteFourKeyItemsCondition, - "elite_four_pokedex_condition": EliteFourPokedexCondition, - "victory_road_condition": VictoryRoadCondition, - "route_22_gate_condition": Route22GateCondition, - "viridian_gym_condition": ViridianGymCondition, - "cerulean_cave_badges_condition": CeruleanCaveBadgesCondition, - "cerulean_cave_key_items_condition": CeruleanCaveKeyItemsCondition, - "route_3_condition": Route3Condition, - "robbed_house_officer": RobbedHouseOfficer, - "second_fossil_check_condition": SecondFossilCheckCondition, - "fossil_check_item_types": FossilCheckItemTypes, - "exp_all": ExpAll, - "old_man": OldMan, - "badgesanity": BadgeSanity, - "badges_needed_for_hm_moves": BadgesNeededForHMMoves, - "key_items_only": KeyItemsOnly, - "tea": Tea, - "extra_key_items": ExtraKeyItems, - "split_card_key": SplitCardKey, - "all_elevators_locked": AllElevatorsLocked, - "extra_strength_boulders": ExtraStrengthBoulders, - "require_item_finder": RequireItemFinder, - "randomize_hidden_items": RandomizeHiddenItems, - "prizesanity": PrizeSanity, - "trainersanity": TrainerSanity, - "dexsanity": DexSanity, - "randomize_pokedex": RandomizePokedex, - "require_pokedex": RequirePokedex, - "all_pokemon_seen": AllPokemonSeen, - "oaks_aide_rt_2": OaksAidRt2, - "oaks_aide_rt_11": OaksAidRt11, - "oaks_aide_rt_15": OaksAidRt15, - "stonesanity": Stonesanity, - "door_shuffle": DoorShuffle, - "warp_tile_shuffle": WarpTileShuffle, - "randomize_rock_tunnel": RandomizeRockTunnel, - "dark_rock_tunnel_logic": DarkRockTunnelLogic, - "free_fly_location": FreeFlyLocation, - "town_map_fly_location": TownMapFlyLocation, - "blind_trainers": BlindTrainers, - "minimum_steps_between_encounters": MinimumStepsBetweenEncounters, - "level_scaling": LevelScaling, - "exp_modifier": ExpModifier, - "randomize_wild_pokemon": RandomizeWildPokemon, - "area_1_to_1_mapping": Area1To1Mapping, - "randomize_starter_pokemon": RandomizeStarterPokemon, - "randomize_static_pokemon": RandomizeStaticPokemon, - "randomize_legendary_pokemon": RandomizeLegendaryPokemon, - "catch_em_all": CatchEmAll, - "randomize_pokemon_stats": RandomizePokemonStats, - "randomize_pokemon_catch_rates": RandomizePokemonCatchRates, - "minimum_catch_rate": MinimumCatchRate, - "randomize_trainer_parties": RandomizeTrainerParties, - "trainer_legendaries": TrainerLegendaries, - "move_balancing": MoveBalancing, - "fix_combat_bugs": FixCombatBugs, - "randomize_pokemon_movesets": RandomizePokemonMovesets, - "confine_transform_to_ditto": ConfineTranstormToDitto, - "start_with_four_moves": StartWithFourMoves, - "same_type_attack_bonus": SameTypeAttackBonus, - "randomize_tm_moves": RandomizeTMMoves, - "tm_same_type_compatibility": TMSameTypeCompatibility, - "tm_normal_type_compatibility": TMNormalTypeCompatibility, - "tm_other_type_compatibility": TMOtherTypeCompatibility, - "hm_same_type_compatibility": HMSameTypeCompatibility, - "hm_normal_type_compatibility": HMNormalTypeCompatibility, - "hm_other_type_compatibility": HMOtherTypeCompatibility, - "inherit_tm_hm_compatibility": InheritTMHMCompatibility, - "randomize_move_types": RandomizeMoveTypes, - "randomize_pokemon_types": RandomizePokemonTypes, - "secondary_type_chance": SecondaryTypeChance, - "randomize_type_chart": RandomizeTypeChart, - "normal_matchups": NormalMatchups, - "super_effective_matchups": SuperEffectiveMatchups, - "not_very_effective_matchups": NotVeryEffectiveMatchups, - "immunity_matchups": ImmunityMatchups, - "safari_zone_normal_battles": SafariZoneNormalBattles, - "normalize_encounter_chances": NormalizeEncounterChances, - "reusable_tms": ReusableTMs, - "better_shops": BetterShops, - "master_ball_price": MasterBallPrice, - "starting_money": StartingMoney, - "lose_money_on_blackout": LoseMoneyOnBlackout, - "poke_doll_skip": PokeDollSkip, - "bicycle_gate_skips": BicycleGateSkips, - "trap_percentage": TrapPercentage, - "poison_trap_weight": PoisonTrapWeight, - "fire_trap_weight": FireTrapWeight, - "paralyze_trap_weight": ParalyzeTrapWeight, - "sleep_trap_weight": SleepTrapWeight, - "ice_trap_weight": IceTrapWeight, - "randomize_pokemon_palettes": RandomizePokemonPalettes, - "death_link": DeathLink -} +@dataclass +class PokemonRBOptions(PerGameCommonOptions): + accessibility: ItemsAccessibility + game_version: GameVersion + trainer_name: TrainerName + rival_name: RivalName + # goal: Goal + elite_four_badges_condition: EliteFourBadgesCondition + elite_four_key_items_condition: EliteFourKeyItemsCondition + elite_four_pokedex_condition: EliteFourPokedexCondition + victory_road_condition: VictoryRoadCondition + route_22_gate_condition: Route22GateCondition + viridian_gym_condition: ViridianGymCondition + cerulean_cave_badges_condition: CeruleanCaveBadgesCondition + cerulean_cave_key_items_condition: CeruleanCaveKeyItemsCondition + route_3_condition: Route3Condition + robbed_house_officer: RobbedHouseOfficer + second_fossil_check_condition: SecondFossilCheckCondition + fossil_check_item_types: FossilCheckItemTypes + exp_all: ExpAll + old_man: OldMan + badgesanity: BadgeSanity + badges_needed_for_hm_moves: BadgesNeededForHMMoves + key_items_only: KeyItemsOnly + tea: Tea + extra_key_items: ExtraKeyItems + split_card_key: SplitCardKey + all_elevators_locked: AllElevatorsLocked + extra_strength_boulders: ExtraStrengthBoulders + require_item_finder: RequireItemFinder + randomize_hidden_items: RandomizeHiddenItems + prizesanity: PrizeSanity + trainersanity: TrainerSanity + dexsanity: DexSanity + randomize_pokedex: RandomizePokedex + require_pokedex: RequirePokedex + all_pokemon_seen: AllPokemonSeen + oaks_aide_rt_2: OaksAidRt2 + oaks_aide_rt_11: OaksAidRt11 + oaks_aide_rt_15: OaksAidRt15 + stonesanity: Stonesanity + door_shuffle: DoorShuffle + warp_tile_shuffle: WarpTileShuffle + randomize_rock_tunnel: RandomizeRockTunnel + dark_rock_tunnel_logic: DarkRockTunnelLogic + free_fly_location: FreeFlyLocation + town_map_fly_location: TownMapFlyLocation + blind_trainers: BlindTrainers + minimum_steps_between_encounters: MinimumStepsBetweenEncounters + level_scaling: LevelScaling + exp_modifier: ExpModifier + randomize_wild_pokemon: RandomizeWildPokemon + area_1_to_1_mapping: Area1To1Mapping + randomize_starter_pokemon: RandomizeStarterPokemon + randomize_static_pokemon: RandomizeStaticPokemon + randomize_legendary_pokemon: RandomizeLegendaryPokemon + catch_em_all: CatchEmAll + randomize_pokemon_stats: RandomizePokemonStats + randomize_pokemon_catch_rates: RandomizePokemonCatchRates + minimum_catch_rate: MinimumCatchRate + randomize_trainer_parties: RandomizeTrainerParties + trainer_legendaries: TrainerLegendaries + move_balancing: MoveBalancing + fix_combat_bugs: FixCombatBugs + randomize_pokemon_movesets: RandomizePokemonMovesets + confine_transform_to_ditto: ConfineTranstormToDitto + start_with_four_moves: StartWithFourMoves + same_type_attack_bonus: SameTypeAttackBonus + randomize_tm_moves: RandomizeTMMoves + tm_same_type_compatibility: TMSameTypeCompatibility + tm_normal_type_compatibility: TMNormalTypeCompatibility + tm_other_type_compatibility: TMOtherTypeCompatibility + hm_same_type_compatibility: HMSameTypeCompatibility + hm_normal_type_compatibility: HMNormalTypeCompatibility + hm_other_type_compatibility: HMOtherTypeCompatibility + inherit_tm_hm_compatibility: InheritTMHMCompatibility + randomize_move_types: RandomizeMoveTypes + randomize_pokemon_types: RandomizePokemonTypes + secondary_type_chance: SecondaryTypeChance + randomize_type_chart: RandomizeTypeChart + normal_matchups: NormalMatchups + super_effective_matchups: SuperEffectiveMatchups + not_very_effective_matchups: NotVeryEffectiveMatchups + immunity_matchups: ImmunityMatchups + type_chart_seed: TypeChartSeed + safari_zone_normal_battles: SafariZoneNormalBattles + normalize_encounter_chances: NormalizeEncounterChances + reusable_tms: ReusableTMs + better_shops: BetterShops + master_ball_price: MasterBallPrice + starting_money: StartingMoney + lose_money_on_blackout: LoseMoneyOnBlackout + poke_doll_skip: PokeDollSkip + bicycle_gate_skips: BicycleGateSkips + trap_percentage: TrapPercentage + poison_trap_weight: PoisonTrapWeight + fire_trap_weight: FireTrapWeight + paralyze_trap_weight: ParalyzeTrapWeight + sleep_trap_weight: SleepTrapWeight + ice_trap_weight: IceTrapWeight + randomize_pokemon_palettes: RandomizePokemonPalettes + death_link: DeathLink diff --git a/worlds/pokemon_rb/pokemon.py b/worlds/pokemon_rb/pokemon.py index 28098a2c53fe..32c0e36869da 100644 --- a/worlds/pokemon_rb/pokemon.py +++ b/worlds/pokemon_rb/pokemon.py @@ -3,8 +3,8 @@ from .rom_addresses import rom_addresses -def set_mon_palettes(self, random, data): - if self.multiworld.randomize_pokemon_palettes[self.player] == "vanilla": +def set_mon_palettes(world, random, data): + if world.options.randomize_pokemon_palettes == "vanilla": return pallet_map = { "Poison": 0x0F, @@ -25,9 +25,9 @@ def set_mon_palettes(self, random, data): } palettes = [] for mon in poke_data.pokemon_data: - if self.multiworld.randomize_pokemon_palettes[self.player] == "primary_type": - pallet = pallet_map[self.local_poke_data[mon]["type1"]] - elif (self.multiworld.randomize_pokemon_palettes[self.player] == "follow_evolutions" and mon in + if world.options.randomize_pokemon_palettes == "primary_type": + pallet = pallet_map[world.local_poke_data[mon]["type1"]] + elif (world.options.randomize_pokemon_palettes == "follow_evolutions" and mon in poke_data.evolves_from and poke_data.evolves_from[mon] != "Eevee"): pallet = palettes[-1] else: # completely_random or follow_evolutions and it is not an evolved form (except eeveelutions) @@ -93,40 +93,41 @@ def move_power(move_data): return power -def process_move_data(self): - self.local_move_data = deepcopy(poke_data.moves) +def process_move_data(world): + world.local_move_data = deepcopy(poke_data.moves) - if self.multiworld.randomize_move_types[self.player]: - for move, data in self.local_move_data.items(): + if world.options.randomize_move_types: + for move, data in world.local_move_data.items(): if move == "No Move": continue # The chance of randomized moves choosing a normal type move is high, so we want to retain having a higher # rate of normal type moves - data["type"] = self.multiworld.random.choice(list(poke_data.type_ids) + (["Normal"] * 4)) - - if self.multiworld.move_balancing[self.player]: - self.local_move_data["Sing"]["accuracy"] = 30 - self.local_move_data["Sleep Powder"]["accuracy"] = 40 - self.local_move_data["Spore"]["accuracy"] = 50 - self.local_move_data["Sonicboom"]["effect"] = 0 - self.local_move_data["Sonicboom"]["power"] = 50 - self.local_move_data["Dragon Rage"]["effect"] = 0 - self.local_move_data["Dragon Rage"]["power"] = 80 - self.local_move_data["Horn Drill"]["effect"] = 0 - self.local_move_data["Horn Drill"]["power"] = 70 - self.local_move_data["Horn Drill"]["accuracy"] = 90 - self.local_move_data["Guillotine"]["effect"] = 0 - self.local_move_data["Guillotine"]["power"] = 70 - self.local_move_data["Guillotine"]["accuracy"] = 90 - self.local_move_data["Fissure"]["effect"] = 0 - self.local_move_data["Fissure"]["power"] = 70 - self.local_move_data["Fissure"]["accuracy"] = 90 - self.local_move_data["Blizzard"]["accuracy"] = 70 - if self.multiworld.randomize_tm_moves[self.player]: - self.local_tms = self.multiworld.random.sample([move for move in poke_data.moves.keys() if move not in - ["No Move"] + poke_data.hm_moves], 50) + data["type"] = world.random.choice(list(poke_data.type_ids) + (["Normal"] * 4)) + + if world.options.move_balancing: + world.local_move_data["Sing"]["accuracy"] = 30 + world.local_move_data["Sleep Powder"]["accuracy"] = 40 + world.local_move_data["Spore"]["accuracy"] = 50 + world.local_move_data["Sonicboom"]["effect"] = 0 + world.local_move_data["Sonicboom"]["power"] = 50 + world.local_move_data["Dragon Rage"]["effect"] = 0 + world.local_move_data["Dragon Rage"]["power"] = 80 + world.local_move_data["Horn Drill"]["effect"] = 0 + world.local_move_data["Horn Drill"]["power"] = 70 + world.local_move_data["Horn Drill"]["accuracy"] = 90 + world.local_move_data["Guillotine"]["effect"] = 0 + world.local_move_data["Guillotine"]["power"] = 70 + world.local_move_data["Guillotine"]["accuracy"] = 90 + world.local_move_data["Fissure"]["effect"] = 0 + world.local_move_data["Fissure"]["power"] = 70 + world.local_move_data["Fissure"]["accuracy"] = 90 + world.local_move_data["Blizzard"]["accuracy"] = 70 + + if world.options.randomize_tm_moves: + world.local_tms = world.random.sample([move for move in poke_data.moves.keys() if move not in + ["No Move"] + poke_data.hm_moves], 50) else: - self.local_tms = poke_data.tm_moves.copy() + world.local_tms = poke_data.tm_moves.copy() def process_pokemon_data(self): @@ -138,12 +139,12 @@ def process_pokemon_data(self): compat_hms = set() for mon, mon_data in local_poke_data.items(): - if self.multiworld.randomize_pokemon_stats[self.player] == "shuffle": + if self.options.randomize_pokemon_stats == "shuffle": stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]] if mon in poke_data.evolves_from: stat_shuffle_map = local_poke_data[poke_data.evolves_from[mon]]["stat_shuffle_map"] else: - stat_shuffle_map = self.multiworld.random.sample(range(0, 5), 5) + stat_shuffle_map = self.random.sample(range(0, 5), 5) mon_data["stat_shuffle_map"] = stat_shuffle_map mon_data["hp"] = stats[stat_shuffle_map[0]] @@ -151,7 +152,7 @@ def process_pokemon_data(self): mon_data["def"] = stats[stat_shuffle_map[2]] mon_data["spd"] = stats[stat_shuffle_map[3]] mon_data["spc"] = stats[stat_shuffle_map[4]] - elif self.multiworld.randomize_pokemon_stats[self.player] == "randomize": + elif self.options.randomize_pokemon_stats == "randomize": first_run = True while (mon_data["hp"] > 255 or mon_data["atk"] > 255 or mon_data["def"] > 255 or mon_data["spd"] > 255 or mon_data["spc"] > 255 or first_run): @@ -168,9 +169,9 @@ def process_pokemon_data(self): mon_data[stat] = 10 total_stats -= 10 assert total_stats >= 0, f"Error distributing stats for {mon} for player {self.player}" - dist = [self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, - self.multiworld.random.randint(1, 101) / 100, self.multiworld.random.randint(1, 101) / 100, - self.multiworld.random.randint(1, 101) / 100] + dist = [self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100, + self.random.randint(1, 101) / 100, self.random.randint(1, 101) / 100, + self.random.randint(1, 101) / 100] total_dist = sum(dist) mon_data["hp"] += int(round(dist[0] / total_dist * total_stats)) @@ -178,30 +179,30 @@ def process_pokemon_data(self): mon_data["def"] += int(round(dist[2] / total_dist * total_stats)) mon_data["spd"] += int(round(dist[3] / total_dist * total_stats)) mon_data["spc"] += int(round(dist[4] / total_dist * total_stats)) - if self.multiworld.randomize_pokemon_types[self.player]: - if self.multiworld.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from: + if self.options.randomize_pokemon_types: + if self.options.randomize_pokemon_types.value == 1 and mon in poke_data.evolves_from: type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"] type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"] if type1 == type2: - if self.multiworld.secondary_type_chance[self.player].value == -1: + if self.options.secondary_type_chance.value == -1: if mon_data["type1"] != mon_data["type2"]: while type2 == type1: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) - elif self.multiworld.random.randint(1, 100) <= self.multiworld.secondary_type_chance[self.player].value: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type2 = self.random.choice(list(poke_data.type_names.values())) + elif self.random.randint(1, 100) <= self.options.secondary_type_chance.value: + type2 = self.random.choice(list(poke_data.type_names.values())) else: - type1 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type1 = self.random.choice(list(poke_data.type_names.values())) type2 = type1 - if ((self.multiworld.secondary_type_chance[self.player].value == -1 and mon_data["type1"] - != mon_data["type2"]) or self.multiworld.random.randint(1, 100) - <= self.multiworld.secondary_type_chance[self.player].value): + if ((self.options.secondary_type_chance.value == -1 and mon_data["type1"] + != mon_data["type2"]) or self.random.randint(1, 100) + <= self.options.secondary_type_chance.value): while type2 == type1: - type2 = self.multiworld.random.choice(list(poke_data.type_names.values())) + type2 = self.random.choice(list(poke_data.type_names.values())) mon_data["type1"] = type1 mon_data["type2"] = type2 - if self.multiworld.randomize_pokemon_movesets[self.player]: - if self.multiworld.randomize_pokemon_movesets[self.player] == "prefer_types": + if self.options.randomize_pokemon_movesets: + if self.options.randomize_pokemon_movesets == "prefer_types": if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal": chances = [[75, "Normal"]] elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal": @@ -219,9 +220,9 @@ def process_pokemon_data(self): moves = list(poke_data.moves.keys()) for move in ["No Move"] + poke_data.hm_moves: moves.remove(move) - if self.multiworld.confine_transform_to_ditto[self.player]: + if self.options.confine_transform_to_ditto: moves.remove("Transform") - if self.multiworld.start_with_four_moves[self.player]: + if self.options.start_with_four_moves: num_moves = 4 else: num_moves = len([i for i in [mon_data["start move 1"], mon_data["start move 2"], @@ -231,12 +232,12 @@ def process_pokemon_data(self): non_power_moves = [] learnsets[mon] = [] for i in range(num_moves): - if i == 0 and mon == "Ditto" and self.multiworld.confine_transform_to_ditto[self.player]: + if i == 0 and mon == "Ditto" and self.options.confine_transform_to_ditto: move = "Transform" else: - move = get_move(self.local_move_data, moves, chances, self.multiworld.random) - while move == "Transform" and self.multiworld.confine_transform_to_ditto[self.player]: - move = get_move(self.local_move_data, moves, chances, self.multiworld.random) + move = get_move(self.local_move_data, moves, chances, self.random) + while move == "Transform" and self.options.confine_transform_to_ditto: + move = get_move(self.local_move_data, moves, chances, self.random) if self.local_move_data[move]["power"] < 5: non_power_moves.append(move) else: @@ -244,59 +245,58 @@ def process_pokemon_data(self): learnsets[mon].sort(key=lambda move: move_power(self.local_move_data[move])) if learnsets[mon]: for move in non_power_moves: - learnsets[mon].insert(self.multiworld.random.randint(1, len(learnsets[mon])), move) + learnsets[mon].insert(self.random.randint(1, len(learnsets[mon])), move) else: learnsets[mon] = non_power_moves for i in range(1, 5): - if mon_data[f"start move {i}"] != "No Move" or self.multiworld.start_with_four_moves[self.player]: + if mon_data[f"start move {i}"] != "No Move" or self.options.start_with_four_moves: mon_data[f"start move {i}"] = learnsets[mon].pop(0) - if self.multiworld.randomize_pokemon_catch_rates[self.player]: - mon_data["catch rate"] = self.multiworld.random.randint(self.multiworld.minimum_catch_rate[self.player], - 255) + if self.options.randomize_pokemon_catch_rates: + mon_data["catch rate"] = self.random.randint(self.options.minimum_catch_rate, 255) else: - mon_data["catch rate"] = max(self.multiworld.minimum_catch_rate[self.player], mon_data["catch rate"]) + mon_data["catch rate"] = max(self.options.minimum_catch_rate, mon_data["catch rate"]) def roll_tm_compat(roll_move): if self.local_move_data[roll_move]["type"] in [mon_data["type1"], mon_data["type2"]]: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_same_type_compatibility[self.player].value == -1: + if self.options.hm_same_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_same_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_same_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_same_type_compatibility[self.player].value == -1: + if self.options.tm_same_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_same_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_same_type_compatibility.value elif self.local_move_data[roll_move]["type"] == "Normal" and "Normal" not in [mon_data["type1"], mon_data["type2"]]: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_normal_type_compatibility[self.player].value == -1: + if self.options.hm_normal_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_normal_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_normal_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_normal_type_compatibility[self.player].value == -1: + if self.options.tm_normal_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_normal_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_normal_type_compatibility.value else: if roll_move in poke_data.hm_moves: - if self.multiworld.hm_other_type_compatibility[self.player].value == -1: + if self.options.hm_other_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - r = self.multiworld.random.randint(1, 100) <= self.multiworld.hm_other_type_compatibility[self.player].value + r = self.random.randint(1, 100) <= self.options.hm_other_type_compatibility.value if r and mon not in poke_data.legendary_pokemon: compat_hms.add(roll_move) return r else: - if self.multiworld.tm_other_type_compatibility[self.player].value == -1: + if self.options.tm_other_type_compatibility.value == -1: return mon_data["tms"][int(flag / 8)] & 1 << (flag % 8) - return self.multiworld.random.randint(1, 100) <= self.multiworld.tm_other_type_compatibility[self.player].value + return self.random.randint(1, 100) <= self.options.tm_other_type_compatibility.value for flag, tm_move in enumerate(tms_hms): - if mon in poke_data.evolves_from and self.multiworld.inherit_tm_hm_compatibility[self.player]: + if mon in poke_data.evolves_from and self.options.inherit_tm_hm_compatibility: if local_poke_data[poke_data.evolves_from[mon]]["tms"][int(flag / 8)] & 1 << (flag % 8): # always inherit learnable tms/hms @@ -310,7 +310,7 @@ def roll_tm_compat(roll_move): # so this gets full chance roll bit = roll_tm_compat(tm_move) # otherwise 50% reduced chance to add compatibility over pre-evolved form - elif self.multiworld.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): + elif self.random.randint(1, 100) > 50 and roll_tm_compat(tm_move): bit = 1 else: bit = 0 @@ -322,15 +322,13 @@ def roll_tm_compat(roll_move): mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) hm_verify = ["Surf", "Strength"] - if self.multiworld.accessibility[self.player] != "minimal" or ((not - self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player], - self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player]) - > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")): + if self.options.accessibility != "minimal" or ((not + self.options.badgesanity) and max(self.options.elite_four_badges_condition, + self.options.route_22_gate_condition, self.options.victory_road_condition) + > 7) or (self.options.door_shuffle not in ("off", "simple")): hm_verify += ["Cut"] - if self.multiworld.accessibility[self.player] != "minimal" or (not - self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or - self.multiworld.extra_key_items[self.player]) - or self.multiworld.door_shuffle[self.player]): + if (self.options.accessibility != "minimal" or (not self.options.dark_rock_tunnel_logic) and + ((self.options.trainersanity or self.options.extra_key_items) or self.options.door_shuffle)): hm_verify += ["Flash"] # Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable # regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for @@ -339,8 +337,7 @@ def roll_tm_compat(roll_move): for hm_move in hm_verify: if hm_move not in compat_hms: - mon = self.multiworld.random.choice([mon for mon in poke_data.pokemon_data if mon not in - poke_data.legendary_pokemon]) + mon = self.random.choice([mon for mon in poke_data.pokemon_data if mon not in poke_data.legendary_pokemon]) flag = tms_hms.index(hm_move) local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) @@ -352,7 +349,7 @@ def verify_hm_moves(multiworld, world, player): def intervene(move, test_state): move_bit = pow(2, poke_data.hm_moves.index(move) + 2) viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit] - if multiworld.randomize_wild_pokemon[player] and viable_mons: + if world.options.randomize_wild_pokemon and viable_mons: accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if loc.type == "Wild Encounter"] @@ -364,7 +361,7 @@ def number_of_zones(mon): placed_mons = [slot.item.name for slot in accessible_slots] - if multiworld.area_1_to_1_mapping[player]: + if world.options.area_1_to_1_mapping: placed_mons.sort(key=lambda i: number_of_zones(i)) else: # this sort method doesn't work if you reference the same list being sorted in the lambda @@ -372,10 +369,10 @@ def number_of_zones(mon): placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) placed_mon = placed_mons.pop() - replace_mon = multiworld.random.choice(viable_mons) - replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name + replace_mon = world.random.choice(viable_mons) + replace_slot = world.random.choice([slot for slot in accessible_slots if slot.item.name == placed_mon]) - if multiworld.area_1_to_1_mapping[player]: + if world.options.area_1_to_1_mapping: zone = " - ".join(replace_slot.name.split(" - ")[:-1]) replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name == placed_mon] @@ -387,7 +384,7 @@ def number_of_zones(mon): tms_hms = world.local_tms + poke_data.hm_moves flag = tms_hms.index(move) mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)] - multiworld.random.shuffle(mon_list) + world.random.shuffle(mon_list) mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]]) for mon in mon_list: @@ -399,31 +396,31 @@ def number_of_zones(mon): while True: intervene_move = None test_state = multiworld.get_all_state(False) - if not logic.can_learn_hm(test_state, "Surf", player): + if not logic.can_learn_hm(test_state, world, "Surf", player): intervene_move = "Surf" - elif not logic.can_learn_hm(test_state, "Strength", player): + elif not logic.can_learn_hm(test_state, world, "Strength", player): intervene_move = "Strength" # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, # as you will require cut to access celadon gyn - elif ((not logic.can_learn_hm(test_state, "Cut", player)) and - (multiworld.accessibility[player] != "minimal" or ((not - multiworld.badgesanity[player]) and max( - multiworld.elite_four_badges_condition[player], - multiworld.route_22_gate_condition[player], - multiworld.victory_road_condition[player]) - > 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))): + elif ((not logic.can_learn_hm(test_state, world, "Cut", player)) and + (world.options.accessibility != "minimal" or ((not + world.options.badgesanity) and max( + world.options.elite_four_badges_condition, + world.options.route_22_gate_condition, + world.options.victory_road_condition) + > 7) or (world.options.door_shuffle not in ("off", "simple")))): intervene_move = "Cut" - elif ((not logic.can_learn_hm(test_state, "Flash", player)) - and multiworld.dark_rock_tunnel_logic[player] - and (multiworld.accessibility[player] != "minimal" - or multiworld.door_shuffle[player])): + elif ((not logic.can_learn_hm(test_state, world, "Flash", player)) + and world.options.dark_rock_tunnel_logic + and (world.options.accessibility != "minimal" + or world.options.door_shuffle)): intervene_move = "Flash" # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps # as reachable, and if on no door shuffle or simple, fly is simply never necessary. # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been # considered in door shuffle. - elif ((not logic.can_learn_hm(test_state, "Fly", player)) - and multiworld.door_shuffle[player] not in + elif ((not logic.can_learn_hm(test_state, world, "Fly", player)) + and world.options.door_shuffle not in ("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]): intervene_move = "Fly" if intervene_move: @@ -432,4 +429,4 @@ def number_of_zones(mon): intervene(intervene_move, test_state) last_intervene = intervene_move else: - break \ No newline at end of file + break diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index 938c39b32090..575f4a61ca6f 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1409,21 +1409,20 @@ def pair(a, b): ['Route 2-E to Route 2 Gate', 'Route 2-SE to Route 2 Gate'], ['Cerulean City-Badge House Backyard to Cerulean Badge House', 'Cerulean City to Cerulean Badge House'], - ['Cerulean City-T to Cerulean Trashed House', - 'Cerulean City-Outskirts to Cerulean Trashed House'], - ['Fuchsia City to Fuchsia Good Rod House', - 'Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House'], - ['Route 11-E to Route 11 Gate 1F', 'Route 11-C to Route 11 Gate 1F'], - ['Route 12-N to Route 12 Gate 1F', 'Route 12-L to Route 12 Gate 1F'], - ['Route 15 to Route 15 Gate 1F', 'Route 15-W to Route 15 Gate 1F'], - ['Route 16-NE to Route 16 Gate 1F-N', 'Route 16-NW to Route 16 Gate 1F-N'], + ['Cerulean City-Outskirts to Cerulean Trashed House', + 'Cerulean City-T to Cerulean Trashed House',], + ['Fuchsia City-Good Rod House Backyard to Fuchsia Good Rod House', 'Fuchsia City to Fuchsia Good Rod House'], + ['Route 11-C to Route 11 Gate 1F', 'Route 11-E to Route 11 Gate 1F'], + ['Route 12-L to Route 12 Gate 1F', 'Route 12-N to Route 12 Gate 1F'], + ['Route 15-W to Route 15 Gate 1F', 'Route 15 to Route 15 Gate 1F'], + ['Route 16-NW to Route 16 Gate 1F-N', 'Route 16-NE to Route 16 Gate 1F-N'], ['Route 16-SW to Route 16 Gate 1F-W', 'Route 16-C to Route 16 Gate 1F-E'], ['Route 18-W to Route 18 Gate 1F-W', 'Route 18-E to Route 18 Gate 1F-E'], ['Route 5 to Route 5 Gate-N', 'Route 5-S to Route 5 Gate-S'], - ['Route 6 to Route 6 Gate-S', 'Route 6-N to Route 6 Gate-N'], + ['Route 6-N to Route 6 Gate-N', 'Route 6 to Route 6 Gate-S'], ['Route 7 to Route 7 Gate-W', 'Route 7-E to Route 7 Gate-E'], - ['Route 8 to Route 8 Gate-E', 'Route 8-W to Route 8 Gate-W'], - ['Route 22 to Route 22 Gate-S', 'Route 23-S to Route 22 Gate-N'] + ['Route 8-W to Route 8 Gate-W', 'Route 8 to Route 8 Gate-E',], + ['Route 23-S to Route 22 Gate-N', 'Route 22 to Route 22 Gate-S'] ] dungeons = [ @@ -1484,7 +1483,7 @@ def create_region(multiworld: MultiWorld, player: int, name: str, locations_per_ for location in locations_per_region.get(name, []): location.parent_region = ret ret.locations.append(location) - if multiworld.randomize_hidden_items[player] == "exclude" and "Hidden" in location.name: + if multiworld.worlds[player].options.randomize_hidden_items == "exclude" and "Hidden" in location.name: location.progress_type = LocationProgressType.EXCLUDED if exits: for exit in exits: @@ -1500,32 +1499,34 @@ def outdoor_map(name): return False -def create_regions(self): - multiworld = self.multiworld - player = self.player +def create_regions(world): + multiworld = world.multiworld + player = world.player locations_per_region = {} - start_inventory = self.multiworld.start_inventory[self.player].value.copy() - if self.multiworld.randomize_pokedex[self.player] == "start_with": + start_inventory = world.options.start_inventory.value.copy() + if world.options.randomize_pokedex == "start_with": start_inventory["Pokedex"] = 1 - self.multiworld.push_precollected(self.create_item("Pokedex")) - if self.multiworld.exp_all[self.player] == "start_with": + world.multiworld.push_precollected(world.create_item("Pokedex")) + if world.options.exp_all == "start_with": start_inventory["Exp. All"] = 1 - self.multiworld.push_precollected(self.create_item("Exp. All")) + world.multiworld.push_precollected(world.create_item("Exp. All")) + + world.item_pool = [] + combined_traps = (world.options.poison_trap_weight.value + + world.options.fire_trap_weight.value + + world.options.paralyze_trap_weight.value + + world.options.ice_trap_weight.value + + world.options.sleep_trap_weight.value) - self.item_pool = [] - combined_traps = (self.multiworld.poison_trap_weight[self.player].value - + self.multiworld.fire_trap_weight[self.player].value - + self.multiworld.paralyze_trap_weight[self.player].value - + self.multiworld.ice_trap_weight[self.player].value) stones = ["Moon Stone", "Fire Stone", "Water Stone", "Thunder Stone", "Leaf Stone"] for location in location_data: locations_per_region.setdefault(location.region, []) # The check for list is so that we don't try to check the item table with a list as a key - if location.inclusion(multiworld, player) and (isinstance(location.original_item, list) or - not (self.multiworld.key_items_only[self.player] and item_table[location.original_item].classification - not in (ItemClassification.progression, ItemClassification.progression_skip_balancing) and not + if location.inclusion(world, player) and (isinstance(location.original_item, list) or + not (world.options.key_items_only and item_table[location.original_item].classification + not in (ItemClassification.progression, ItemClassification.progression_skip_balancing) and not location.event)): location_object = PokemonRBLocation(player, location.name, location.address, location.rom_address, location.type, location.level, location.level_address) @@ -1535,51 +1536,53 @@ def create_regions(self): event = location.event if location.original_item is None: - item = self.create_filler() - elif location.original_item == "Exp. All" and self.multiworld.exp_all[self.player] == "remove": - item = self.create_filler() + item = world.create_filler() + elif location.original_item == "Exp. All" and world.options.exp_all == "remove": + item = world.create_filler() elif location.original_item == "Pokedex": - if self.multiworld.randomize_pokedex[self.player] == "vanilla": + if world.options.randomize_pokedex == "vanilla": + location_object.event = True event = True - item = self.create_item("Pokedex") - elif location.original_item == "Moon Stone" and self.multiworld.stonesanity[self.player]: + item = world.create_item("Pokedex") + elif location.original_item == "Moon Stone" and world.options.stonesanity: stone = stones.pop() - item = self.create_item(stone) + item = world.create_item(stone) elif location.original_item.startswith("TM"): - if self.multiworld.randomize_tm_moves[self.player]: - item = self.create_item(location.original_item.split(" ")[0]) + if world.options.randomize_tm_moves: + item = world.create_item(location.original_item.split(" ")[0]) else: - item = self.create_item(location.original_item) - elif location.original_item == "Card Key" and self.multiworld.split_card_key[self.player] == "on": - item = self.create_item("Card Key 3F") - elif "Card Key" in location.original_item and self.multiworld.split_card_key[self.player] == "progressive": - item = self.create_item("Progressive Card Key") + item = world.create_item(location.original_item) + elif location.original_item == "Card Key" and world.options.split_card_key == "on": + item = world.create_item("Card Key 3F") + elif "Card Key" in location.original_item and world.options.split_card_key == "progressive": + item = world.create_item("Progressive Card Key") else: - item = self.create_item(location.original_item) - if (item.classification == ItemClassification.filler and self.multiworld.random.randint(1, 100) - <= self.multiworld.trap_percentage[self.player].value and combined_traps != 0): - item = self.create_item(self.select_trap()) + item = world.create_item(location.original_item) + if (item.classification == ItemClassification.filler and world.random.randint(1, 100) + <= world.options.trap_percentage.value and combined_traps != 0): + item = world.create_item(world.select_trap()) - if self.multiworld.key_items_only[self.player] and (not location.event) and (not item.advancement) and location.original_item != "Exp. All": + if (world.options.key_items_only and (location.original_item != "Exp. All") + and not (location.event or item.advancement)): continue if item.name in start_inventory and start_inventory[item.name] > 0 and \ location.original_item in item_groups["Unique"]: start_inventory[location.original_item] -= 1 - item = self.create_filler() + item = world.create_filler() if event: location_object.place_locked_item(item) if location.type == "Trainer Parties": location_object.party_data = deepcopy(location.party_data) else: - self.item_pool.append(item) + world.item_pool.append(item) - self.multiworld.random.shuffle(self.item_pool) - advancement_items = [item.name for item in self.item_pool if item.advancement] \ - + [item.name for item in self.multiworld.precollected_items[self.player] if + world.random.shuffle(world.item_pool) + advancement_items = [item.name for item in world.item_pool if item.advancement] \ + + [item.name for item in world.multiworld.precollected_items[world.player] if item.advancement] - self.total_key_items = len( + world.total_key_items = len( # The stonesanity items are not checked for here and instead just always added as the `+ 4` # They will always exist, but if stonesanity is off, then only as events. # We don't want to just add 4 if stonesanity is off while still putting them in this list in case @@ -1589,15 +1592,16 @@ def create_regions(self): "Secret Key", "Poke Flute", "Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "Card Key 2F", "Card Key 3F", "Card Key 4F", "Card Key 5F", "Card Key 6F", "Card Key 7F", "Card Key 8F", "Card Key 9F", "Card Key 10F", - "Card Key 11F", "Exp. All", "Moon Stone"] if item in advancement_items]) + 4 + "Card Key 11F", "Exp. All", "Moon Stone", "Oak's Parcel", "Helix Fossil", "Dome Fossil", + "Old Amber", "Tea", "Gold Teeth", "Bike Voucher"] if item in advancement_items]) + 4 if "Progressive Card Key" in advancement_items: - self.total_key_items += 10 + world.total_key_items += 10 - self.multiworld.cerulean_cave_key_items_condition[self.player].total = \ - int((self.total_key_items / 100) * self.multiworld.cerulean_cave_key_items_condition[self.player].value) + world.options.cerulean_cave_key_items_condition.total = \ + int((world.total_key_items / 100) * world.options.cerulean_cave_key_items_condition.value) - self.multiworld.elite_four_key_items_condition[self.player].total = \ - int((self.total_key_items / 100) * self.multiworld.elite_four_key_items_condition[self.player].value) + world.options.elite_four_key_items_condition.total = \ + int((world.total_key_items / 100) * world.options.elite_four_key_items_condition.value) regions = [create_region(multiworld, player, region, locations_per_region) for region in warp_data] multiworld.regions += regions @@ -1609,7 +1613,7 @@ def create_regions(self): connect(multiworld, player, "Menu", "Pokedex", one_way=True) connect(multiworld, player, "Menu", "Evolution", one_way=True) connect(multiworld, player, "Menu", "Fossil", lambda state: logic.fossil_checks(state, - state.multiworld.second_fossil_check_condition[player].value, player), one_way=True) + world.options.second_fossil_check_condition.value, player), one_way=True) connect(multiworld, player, "Pallet Town", "Route 1") connect(multiworld, player, "Route 1", "Viridian City") connect(multiworld, player, "Viridian City", "Route 22") @@ -1617,24 +1621,24 @@ def create_regions(self): connect(multiworld, player, "Route 2-SW", "Route 2-Grass", one_way=True) connect(multiworld, player, "Route 2-NW", "Route 2-Grass", one_way=True) connect(multiworld, player, "Route 22 Gate-S", "Route 22 Gate-N", - lambda state: logic.has_badges(state, state.multiworld.route_22_gate_condition[player].value, player)) - connect(multiworld, player, "Route 23-Grass", "Route 23-C", lambda state: logic.has_badges(state, state.multiworld.victory_road_condition[player].value, player)) - connect(multiworld, player, "Route 23-Grass", "Route 23-S", lambda state: logic.can_surf(state, player)) + lambda state: logic.has_badges(state, world.options.route_22_gate_condition.value, player)) + connect(multiworld, player, "Route 23-Grass", "Route 23-C", lambda state: logic.has_badges(state, world.options.victory_road_condition.value, player)) + connect(multiworld, player, "Route 23-Grass", "Route 23-S", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Viridian City-N", "Viridian City-G", lambda state: - logic.has_badges(state, state.multiworld.viridian_gym_condition[player].value, player)) - connect(multiworld, player, "Route 2-SW", "Route 2-SE", lambda state: logic.can_cut(state, player)) - connect(multiworld, player, "Route 2-NW", "Route 2-NE", lambda state: logic.can_cut(state, player)) - connect(multiworld, player, "Route 2-E", "Route 2-NE", lambda state: logic.can_cut(state, player)) + logic.has_badges(state, world.options.viridian_gym_condition.value, player)) + connect(multiworld, player, "Route 2-SW", "Route 2-SE", lambda state: logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 2-NW", "Route 2-NE", lambda state: logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 2-E", "Route 2-NE", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Route 2-SW", "Viridian City-N") connect(multiworld, player, "Route 2-NW", "Pewter City") connect(multiworld, player, "Pewter City", "Pewter City-E") connect(multiworld, player, "Pewter City-M", "Pewter City", one_way=True) - connect(multiworld, player, "Pewter City", "Pewter City-M", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Pewter City-E", "Route 3", lambda state: logic.route_3(state, player), one_way=True) + connect(multiworld, player, "Pewter City", "Pewter City-M", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Pewter City-E", "Route 3", lambda state: logic.route(state, world, player), one_way=True) connect(multiworld, player, "Route 3", "Pewter City-E", one_way=True) connect(multiworld, player, "Route 4-W", "Route 3") - connect(multiworld, player, "Route 24", "Cerulean City-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean City-Water", "Route 4-Lass", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Route 24", "Cerulean City-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean City-Water", "Route 4-Lass", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Mt Moon B2F", "Mt Moon B2F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B2F-NE", "Mt Moon B2F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B2F-C", "Mt Moon B2F-Wild", one_way=True) @@ -1644,14 +1648,14 @@ def create_regions(self): connect(multiworld, player, "Cerulean City", "Route 24") connect(multiworld, player, "Cerulean City", "Cerulean City-T", lambda state: state.has("Help Bill", player)) connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", one_way=True) - connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Cerulean City-Outskirts", "Route 5") - connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Route 24", "Route 25") connect(multiworld, player, "Route 9", "Route 10-N") - connect(multiworld, player, "Route 10-N", "Route 10-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 10-C", "Route 10-P", lambda state: state.has("Plant Key", player) or not state.multiworld.extra_key_items[player].value) + connect(multiworld, player, "Route 10-N", "Route 10-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 10-C", "Route 10-P", lambda state: state.has("Plant Key", player) or not world.options.extra_key_items.value) connect(multiworld, player, "Pallet Town", "Pallet/Viridian Fishing", lambda state: state.has("Super Rod", player), one_way=True) connect(multiworld, player, "Viridian City", "Pallet/Viridian Fishing", lambda state: state.has("Super Rod", player), one_way=True) connect(multiworld, player, "Route 22", "Route 22 Fishing", lambda state: state.has("Super Rod", player), one_way=True) @@ -1697,10 +1701,10 @@ def create_regions(self): connect(multiworld, player, "Pallet Town", "Old Rod Fishing", lambda state: state.has("Old Rod", player), one_way=True) connect(multiworld, player, "Pallet Town", "Good Rod Fishing", lambda state: state.has("Good Rod", player), one_way=True) connect(multiworld, player, "Cinnabar Lab Fossil Room", "Fossil Level", lambda state: logic.fossil_checks(state, 1, player), one_way=True) - connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 7 Gate-W", "Route 7 Gate-E", lambda state: logic.can_pass_guards(state, player)) - connect(multiworld, player, "Route 8 Gate-W", "Route 8 Gate-E", lambda state: logic.can_pass_guards(state, player)) + connect(multiworld, player, "Route 5 Gate-N", "Route 5 Gate-S", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 6 Gate-N", "Route 6 Gate-S", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 7 Gate-W", "Route 7 Gate-E", lambda state: logic.can_pass_guards(state, world, player)) + connect(multiworld, player, "Route 8 Gate-W", "Route 8 Gate-E", lambda state: logic.can_pass_guards(state, world, player)) connect(multiworld, player, "Saffron City", "Route 5-S") connect(multiworld, player, "Saffron City", "Route 6-N") connect(multiworld, player, "Saffron City", "Route 7-E") @@ -1710,59 +1714,59 @@ def create_regions(self): connect(multiworld, player, "Saffron City", "Saffron City-G", lambda state: state.has("Silph Co Liberated", player)) connect(multiworld, player, "Saffron City", "Saffron City-Silph", lambda state: state.has("Fuji Saved", player)) connect(multiworld, player, "Route 6", "Vermilion City") - connect(multiworld, player, "Vermilion City", "Vermilion City-G", lambda state: logic.can_surf(state, player) or logic.can_cut(state, player)) + connect(multiworld, player, "Vermilion City", "Vermilion City-G", lambda state: logic.can_surf(state, world, player) or logic.can_cut(state, world, player)) connect(multiworld, player, "Vermilion City", "Vermilion City-Dock", lambda state: state.has("S.S. Ticket", player)) connect(multiworld, player, "Vermilion City", "Route 11") - connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 12-N", "Route 12-S", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 12-W", "Route 11-E", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 12-W", "Route 12-N", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 12-W", "Route 12-S", lambda state: state.has("Poke Flute", player)) - connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 12-L", "Lavender Town") connect(multiworld, player, "Route 10-S", "Lavender Town") connect(multiworld, player, "Route 8", "Lavender Town") - connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and state.multiworld.poke_doll_skip[player])) - connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and world.options.poke_doll_skip)) + connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 7", "Celadon City") - connect(multiworld, player, "Celadon City", "Celadon City-G", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Celadon City", "Celadon City-G", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Celadon City", "Route 16-E") - connect(multiworld, player, "Route 18 Gate 1F-W", "Route 18 Gate 1F-E", lambda state: state.has("Bicycle", player) or state.multiworld.bicycle_gate_skips[player] == "in_logic") - connect(multiworld, player, "Route 16 Gate 1F-W", "Route 16 Gate 1F-E", lambda state: state.has("Bicycle", player) or state.multiworld.bicycle_gate_skips[player] == "in_logic") - connect(multiworld, player, "Route 16-E", "Route 16-NE", lambda state: logic.can_cut(state, player)) + connect(multiworld, player, "Route 18 Gate 1F-W", "Route 18 Gate 1F-E", lambda state: state.has("Bicycle", player) or world.options.bicycle_gate_skips == "in_logic") + connect(multiworld, player, "Route 16 Gate 1F-W", "Route 16 Gate 1F-E", lambda state: state.has("Bicycle", player) or world.options.bicycle_gate_skips == "in_logic") + connect(multiworld, player, "Route 16-E", "Route 16-NE", lambda state: logic.can_cut(state, world, player)) connect(multiworld, player, "Route 16-E", "Route 16-C", lambda state: state.has("Poke Flute", player)) connect(multiworld, player, "Route 17", "Route 16-SW") connect(multiworld, player, "Route 17", "Route 18-W") # connect(multiworld, player, "Pokemon Mansion 2F", "Pokemon Mansion 2F-NW", one_way=True) - connect(multiworld, player, "Safari Zone Gate-S", "Safari Zone Gate-N", lambda state: state.has("Safari Pass", player) or not state.multiworld.extra_key_items[player].value, one_way=True) + connect(multiworld, player, "Safari Zone Gate-S", "Safari Zone Gate-N", lambda state: state.has("Safari Pass", player) or not world.options.extra_key_items.value, one_way=True) connect(multiworld, player, "Fuchsia City", "Route 15-W") connect(multiworld, player, "Fuchsia City", "Route 18-E") connect(multiworld, player, "Route 15", "Route 14") - connect(multiworld, player, "Route 14", "Route 15-N", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Route 14", "Route 14-Grass", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Route 13", "Route 13-Grass", lambda state: logic.can_cut(state, player), one_way=True) + connect(multiworld, player, "Route 14", "Route 15-N", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Route 14", "Route 14-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Route 13", "Route 13-Grass", lambda state: logic.can_cut(state, world, player), one_way=True) connect(multiworld, player, "Route 14", "Route 13") - connect(multiworld, player, "Route 13", "Route 13-E", lambda state: logic.can_strength(state, player) or logic.can_surf(state, player) or not state.multiworld.extra_strength_boulders[player].value) + connect(multiworld, player, "Route 13", "Route 13-E", lambda state: logic.can_strength(state, world, player) or logic.can_surf(state, world, player) or not world.options.extra_strength_boulders.value) connect(multiworld, player, "Route 12-S", "Route 13-E") connect(multiworld, player, "Fuchsia City", "Route 19-N") - connect(multiworld, player, "Route 19-N", "Route 19-S", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 20-E", "Route 20-IW", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 19-N", "Route 19-S", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 20-E", "Route 20-IW", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 20-E", "Route 19-S") - connect(multiworld, player, "Route 20-W", "Cinnabar Island", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Route 20-IE", "Route 20-W", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Route 20-W", "Cinnabar Island", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Route 20-IE", "Route 20-W", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Route 20-E", "Route 19/20-Water", one_way=True) connect(multiworld, player, "Route 20-W", "Route 19/20-Water", one_way=True) connect(multiworld, player, "Route 19-S", "Route 19/20-Water", one_way=True) - connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Safari Zone West", "Safari Zone West-Wild", one_way=True) connect(multiworld, player, "Safari Zone West-NW", "Safari Zone West-Wild", one_way=True) - connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-C", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-C", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Safari Zone Center-S", "Safari Zone Center-Wild", one_way=True) connect(multiworld, player, "Safari Zone Center-NW", "Safari Zone Center-Wild", one_way=True) connect(multiworld, player, "Safari Zone Center-NE", "Safari Zone Center-Wild", one_way=True) - connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F", lambda state: logic.can_strength(state, player)) - connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-S", lambda state: logic.can_strength(state, player), one_way=True) + connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F", lambda state: logic.can_strength(state, world, player)) + connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-S", lambda state: logic.can_strength(state, world, player), one_way=True) connect(multiworld, player, "Victory Road 3F", "Victory Road 3F-Wild", one_way=True) connect(multiworld, player, "Victory Road 3F-SE", "Victory Road 3F-Wild", one_way=True) connect(multiworld, player, "Victory Road 3F-S", "Victory Road 3F-Wild", one_way=True) @@ -1771,10 +1775,10 @@ def create_regions(self): connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-Wild", one_way=True) connect(multiworld, player, "Victory Road 2F-E", "Victory Road 2F-Wild", one_way=True) connect(multiworld, player, "Victory Road 2F-SE", "Victory Road 2F-Wild", one_way=True) - connect(multiworld, player, "Victory Road 2F-W", "Victory Road 2F-C", lambda state: logic.can_strength(state, player), one_way=True) - connect(multiworld, player, "Victory Road 2F-NW", "Victory Road 2F-W", lambda state: logic.can_strength(state, player), one_way=True) - connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-SE", lambda state: logic.can_strength(state, player) and state.has("Victory Road Boulder", player), one_way=True) - connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F", lambda state: logic.can_strength(state, player)) + connect(multiworld, player, "Victory Road 2F-W", "Victory Road 2F-C", lambda state: logic.can_strength(state, world, player), one_way=True) + connect(multiworld, player, "Victory Road 2F-NW", "Victory Road 2F-W", lambda state: logic.can_strength(state, world, player), one_way=True) + connect(multiworld, player, "Victory Road 2F-C", "Victory Road 2F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Victory Road Boulder", player), one_way=True) + connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F", lambda state: logic.can_strength(state, world, player)) connect(multiworld, player, "Victory Road 1F", "Victory Road 1F-Wild", one_way=True) connect(multiworld, player, "Victory Road 1F-S", "Victory Road 1F-Wild", one_way=True) connect(multiworld, player, "Mt Moon B1F-W", "Mt Moon B1F-Wild", one_way=True) @@ -1796,50 +1800,50 @@ def create_regions(self): connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-Wild", one_way=True) connect(multiworld, player, "Seafoam Islands B3F-NE", "Seafoam Islands B3F-Wild", one_way=True) connect(multiworld, player, "Seafoam Islands B3F-SE", "Seafoam Islands B3F-Wild", one_way=True) - connect(multiworld, player, "Seafoam Islands B4F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Seafoam Islands B4F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Seafoam Islands B4F-W", "Seafoam Islands B4F", one_way=True) - connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-SE", lambda state: logic.can_surf(state, player) and logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6)) - connect(multiworld, player, "Viridian City", "Viridian City-N", lambda state: state.has("Oak's Parcel", player) or state.multiworld.old_man[player].value == 2 or logic.can_cut(state, player)) - connect(multiworld, player, "Route 11", "Route 11-C", lambda state: logic.can_strength(state, player) or not state.multiworld.extra_strength_boulders[player]) + connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B3F-SE", lambda state: logic.can_surf(state, world, player) and logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6)) + connect(multiworld, player, "Viridian City", "Viridian City-N", lambda state: state.has("Oak's Parcel", player) or world.options.old_man.value == 2 or logic.can_cut(state, world, player)) + connect(multiworld, player, "Route 11", "Route 11-C", lambda state: logic.can_strength(state, world, player) or not world.options.extra_strength_boulders) connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-G", lambda state: state.has("Secret Key", player)) - connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-M", lambda state: state.has("Mansion Key", player) or not state.multiworld.extra_key_items[player].value) - connect(multiworld, player, "Route 21", "Cinnabar Island", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Pallet Town", "Route 21", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Celadon Gym", "Celadon Gym-C", lambda state: logic.can_cut(state, player), one_way=True) - connect(multiworld, player, "Celadon Game Corner", "Celadon Game Corner-Hidden Stairs", lambda state: (not state.multiworld.extra_key_items[player]) or state.has("Hideout Key", player), one_way=True) + connect(multiworld, player, "Cinnabar Island", "Cinnabar Island-M", lambda state: state.has("Mansion Key", player) or not world.options.extra_key_items.value) + connect(multiworld, player, "Route 21", "Cinnabar Island", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Pallet Town", "Route 21", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Celadon Gym", "Celadon Gym-C", lambda state: logic.can_cut(state, world, player), one_way=True) + connect(multiworld, player, "Celadon Game Corner", "Celadon Game Corner-Hidden Stairs", lambda state: (not world.options.extra_key_items) or state.has("Hideout Key", player), one_way=True) connect(multiworld, player, "Celadon Game Corner-Hidden Stairs", "Celadon Game Corner", one_way=True) connect(multiworld, player, "Rocket Hideout B1F-SE", "Rocket Hideout B1F", one_way=True) - connect(multiworld, player, "Indigo Plateau Lobby", "Indigo Plateau Lobby-N", lambda state: logic.has_badges(state, state.multiworld.elite_four_badges_condition[player].value, player) and logic.has_pokemon(state, state.multiworld.elite_four_pokedex_condition[player].total, player) and logic.has_key_items(state, state.multiworld.elite_four_key_items_condition[player].total, player) and (state.has("Pokedex", player, int(state.multiworld.elite_four_pokedex_condition[player].total > 1) * state.multiworld.require_pokedex[player].value))) + connect(multiworld, player, "Indigo Plateau Lobby", "Indigo Plateau Lobby-N", lambda state: logic.has_badges(state, world.options.elite_four_badges_condition.value, player) and logic.has_pokemon(state, world.options.elite_four_pokedex_condition.total, player) and logic.has_key_items(state, world.options.elite_four_key_items_condition.total, player) and (state.has("Pokedex", player, int(world.options.elite_four_pokedex_condition.total > 1) * world.options.require_pokedex.value))) connect(multiworld, player, "Pokemon Mansion 3F", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SW", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 3F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 2F-E", "Pokemon Mansion 2F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 1F-SE", "Pokemon Mansion 1F-Wild", one_way=True) connect(multiworld, player, "Pokemon Mansion 1F", "Pokemon Mansion 1F-Wild", one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-S 1", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-S 2", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NW 1", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NW 2", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NE 1", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-NE 2", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-W 1", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-W 2", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-E 1", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel B1F-E 2", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, player)) - connect(multiworld, player, "Rock Tunnel 1F-S", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-NW", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel 1F-NE", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel B1F-W", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) - connect(multiworld, player, "Rock Tunnel B1F-E", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-S 1", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-S 2", "Rock Tunnel 1F-S", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NW 1", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NW 2", "Rock Tunnel 1F-NW", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NE 1", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-NE 2", "Rock Tunnel 1F-NE", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-W 1", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-W 2", "Rock Tunnel B1F-W", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-E 1", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel B1F-E 2", "Rock Tunnel B1F-E", lambda state: logic.rock_tunnel(state, world, player)) + connect(multiworld, player, "Rock Tunnel 1F-S", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-NW", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel 1F-NE", "Rock Tunnel 1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel B1F-W", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) + connect(multiworld, player, "Rock Tunnel B1F-E", "Rock Tunnel B1F-Wild", lambda state: logic.rock_tunnel(state, world, player), one_way=True) connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Wild", one_way=True) connect(multiworld, player, "Cerulean Cave 1F-NW", "Cerulean Cave 1F-Wild", one_way=True) - connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) - connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, player)) + connect(multiworld, player, "Cerulean Cave 1F-SE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-SW", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-N", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) + connect(multiworld, player, "Cerulean Cave 1F-NE", "Cerulean Cave 1F-Water", lambda state: logic.can_surf(state, world, player)) connect(multiworld, player, "Pokemon Mansion 3F", "Pokemon Mansion 3F-SE", one_way=True) connect(multiworld, player, "Silph Co 2F", "Silph Co 2F-NW", lambda state: logic.card_key(state, 2, player)) connect(multiworld, player, "Silph Co 2F", "Silph Co 2F-SW", lambda state: logic.card_key(state, 2, player)) @@ -1858,80 +1862,80 @@ def create_regions(self): connect(multiworld, player, "Silph Co 9F-NW", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player)) connect(multiworld, player, "Silph Co 10F", "Silph Co 10F-SE", lambda state: logic.card_key(state, 10, player)) connect(multiworld, player, "Silph Co 11F-W", "Silph Co 11F-C", lambda state: logic.card_key(state, 11, player)) - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-1F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-2F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-3F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-4F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-5F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-6F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-7F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-8F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-9F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-10F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-11F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-1F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-2F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-3F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-4F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-5F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-6F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-7F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-8F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-9F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-10F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Silph Co Elevator", "Silph Co Elevator-11F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B1F", lambda state: state.has("Lift Key", player)) connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B2F", lambda state: state.has("Lift Key", player)) connect(multiworld, player, "Rocket Hideout Elevator", "Rocket Hideout Elevator-B4F", lambda state: state.has("Lift Key", player)) - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-1F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-2F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-3F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-4F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), - connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-5F", lambda state: (not state.multiworld.all_elevators_locked[player]) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-1F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-2F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-3F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-4F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), + connect(multiworld, player, "Celadon Department Store Elevator", "Celadon Department Store Elevator-5F", lambda state: (not world.options.all_elevators_locked) or state.has("Lift Key", player)), connect(multiworld, player, "Route 23-N", "Indigo Plateau") connect(multiworld, player, "Cerulean City-Water", "Cerulean City-Cave", lambda state: - logic.has_badges(state, self.multiworld.cerulean_cave_badges_condition[player].value, player) and - logic.has_key_items(state, self.multiworld.cerulean_cave_key_items_condition[player].total, player) and logic.can_surf(state, player)) + logic.has_badges(state, world.options.cerulean_cave_badges_condition.value, player) and + logic.has_key_items(state, world.options.cerulean_cave_key_items_condition.total, player) and logic.can_surf(state, world, player)) # access to any part of a city will enable flying to the Pokemon Center - connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)") - connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, player), one_way=True) - connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-G to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)") - connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)") - connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True, name="Celadon City-G to Celadon City (Fly)") - connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)") - connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)") - connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)") - connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)") + connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)") + connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, world, player), one_way=True) + connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-G to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)") + connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)") + connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Celadon City-G to Celadon City (Fly)") + connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)") + connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, world, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)") + connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)") + connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, world, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)") # drops connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F (Drop)") connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F-NE (Drop)") connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B2F-NW (Drop)") connect(multiworld, player, "Seafoam Islands B1F-NE", "Seafoam Islands B2F-NE", one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) - connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) + connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F-SE", lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True) # If you haven't dropped the boulders, you'll go straight to B4F connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B4F-W", one_way=True) connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B4F-W", one_way=True) connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True, name="Seafoam Islands B1F to Seafoam Islands B4F (Drop)") - connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True) + connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, world, player), one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 2F", one_way=True) connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 1F-SE", one_way=True) connect(multiworld, player, "Victory Road 3F-S", "Victory Road 2F-C", one_way=True) - if multiworld.worlds[player].fly_map != "Pallet Town": - connect(multiworld, player, "Menu", multiworld.worlds[player].fly_map, - lambda state: logic.can_fly(state, player), one_way=True, name="Free Fly Location") + if world.fly_map != "Pallet Town": + connect(multiworld, player, "Menu", world.fly_map, + lambda state: logic.can_fly(state, world, player), one_way=True, name="Free Fly Location") - if multiworld.worlds[player].town_map_fly_map != "Pallet Town": - connect(multiworld, player, "Menu", multiworld.worlds[player].town_map_fly_map, - lambda state: logic.can_fly(state, player) and state.has("Town Map", player), one_way=True, + if world.town_map_fly_map != "Pallet Town": + connect(multiworld, player, "Menu", world.town_map_fly_map, + lambda state: logic.can_fly(state, world, player) and state.has("Town Map", player), one_way=True, name="Town Map Fly Location") - cache = multiworld.regions.entrance_cache[self.player].copy() - if multiworld.badgesanity[player] or multiworld.door_shuffle[player] in ("off", "simple"): + cache = multiworld.regions.entrance_cache[world.player].copy() + if world.options.badgesanity or world.options.door_shuffle in ("off", "simple"): badges = None badge_locs = None else: - badges = [item for item in self.item_pool if "Badge" in item.name] + badges = [item for item in world.item_pool if "Badge" in item.name] for badge in badges: - self.item_pool.remove(badge) + world.item_pool.remove(badge) badge_locs = [multiworld.get_location(loc, player) for loc in [ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", @@ -1939,15 +1943,18 @@ def create_regions(self): ]] for attempt in range(10): try: - door_shuffle(self, multiworld, player, badges, badge_locs) + door_shuffle(world, multiworld, player, badges, badge_locs) except DoorShuffleException as e: if attempt == 9: raise e - for region in self.multiworld.get_regions(player): + for region in world.multiworld.get_regions(player): for entrance in reversed(region.exits): if isinstance(entrance, PokemonRBWarp): region.exits.remove(entrance) - multiworld.regions.entrance_cache[self.player] = cache.copy() + for entrance in reversed(region.entrances): + if isinstance(entrance, PokemonRBWarp): + region.entrances.remove(entrance) + multiworld.regions.entrance_cache[world.player] = cache.copy() if badge_locs: for loc in badge_locs: loc.item = None @@ -1965,36 +1972,36 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): shuffle = True interior = False if not outdoor_map(region.name) and not outdoor_map(entrance_data['to']['map']): - if multiworld.door_shuffle[player] not in ("full", "insanity", "decoupled"): + if world.options.door_shuffle not in ("full", "insanity", "decoupled"): shuffle = False interior = True - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": if sorted([entrance_data['to']['map'], region.name]) == ["Celadon Game Corner-Hidden Stairs", "Rocket Hideout B1F"]: shuffle = True elif sorted([entrance_data['to']['map'], region.name]) == ["Celadon City", "Celadon Game Corner"]: shuffle = False - if (multiworld.randomize_rock_tunnel[player] and "Rock Tunnel" in region.name and "Rock Tunnel" in + if (world.options.randomize_rock_tunnel and "Rock Tunnel" in region.name and "Rock Tunnel" in entrance_data['to']['map']): shuffle = False elif (f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else entrance_data["name"]) in silph_co_warps + saffron_gym_warps: - if multiworld.warp_tile_shuffle[player]: + if world.options.warp_tile_shuffle: shuffle = True - if multiworld.warp_tile_shuffle[player] == "mixed" and multiworld.door_shuffle[player] == "full": + if world.options.warp_tile_shuffle == "mixed" and world.options.door_shuffle == "full": interior = True else: interior = False else: shuffle = False - elif not multiworld.door_shuffle[player]: + elif not world.options.door_shuffle: shuffle = False if shuffle: entrance = PokemonRBWarp(player, f"{region.name} to {entrance_data['to']['map']}" if "name" not in entrance_data else entrance_data["name"], region, entrance_data["id"], entrance_data["address"], entrance_data["flags"] if "flags" in entrance_data else "") - if interior and multiworld.door_shuffle[player] == "full": + if interior and world.options.door_shuffle == "full": full_interiors.append(entrance) else: entrances.append(entrance) @@ -2006,22 +2013,22 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections = set() one_way_forced_connections = set() - if multiworld.door_shuffle[player]: - if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"): + if world.options.door_shuffle: + if world.options.door_shuffle in ("full", "insanity", "decoupled"): safari_zone_doors = [door for pair in safari_zone_connections for door in pair] safari_zone_doors.sort() order = ["Center", "East", "North", "West"] - multiworld.random.shuffle(order) + world.random.shuffle(order) usable_doors = ["Safari Zone Gate-N to Safari Zone Center-S"] for section in order: section_doors = [door for door in safari_zone_doors if door.startswith(f"Safari Zone {section}")] - connect_door_a = multiworld.random.choice(usable_doors) - connect_door_b = multiworld.random.choice(section_doors) + connect_door_a = world.random.choice(usable_doors) + connect_door_b = world.random.choice(section_doors) usable_doors.remove(connect_door_a) section_doors.remove(connect_door_b) forced_connections.add((connect_door_a, connect_door_b)) usable_doors += section_doors - multiworld.random.shuffle(usable_doors) + world.random.shuffle(usable_doors) while usable_doors: forced_connections.add((usable_doors.pop(), usable_doors.pop())) else: @@ -2029,32 +2036,32 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): usable_safe_rooms = safe_rooms.copy() - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": forced_connections.update(simple_mandatory_connections) else: usable_safe_rooms += pokemarts - if multiworld.key_items_only[player]: + if world.options.key_items_only: usable_safe_rooms.remove("Viridian Pokemart to Viridian City") - if multiworld.door_shuffle[player] in ("full", "insanity", "decoupled"): + if world.options.door_shuffle in ("full", "insanity", "decoupled"): forced_connections.update(full_mandatory_connections) - r = multiworld.random.randint(0, 3) + r = world.random.randint(0, 3) if r == 2: forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", "Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E")) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + mansion_dead_ends + world.random.choice(mansion_stair_destinations + mansion_dead_ends + ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"]))) - if multiworld.door_shuffle[player] == "full": + if world.options.door_shuffle == "full": forced_connections.add(("Pokemon Mansion 1F to Pokemon Mansion 2F", "Pokemon Mansion 3F to Pokemon Mansion 2F")) elif r == 3: - dead_end = multiworld.random.randint(0, 1) + dead_end = world.random.randint(0, 1) forced_connections.add(("Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E", mansion_dead_ends[dead_end])) forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", "Pokemon Mansion B1F to Pokemon Mansion 1F-SE")) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + world.random.choice(mansion_stair_destinations + [mansion_dead_ends[dead_end ^ 1]]))) else: forced_connections.add(("Pokemon Mansion 3F-SE to Pokemon Mansion 2F-E", @@ -2062,40 +2069,40 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections.add(("Pokemon Mansion 1F-SE to Pokemon Mansion B1F", mansion_dead_ends[r ^ 1])) forced_connections.add(("Pokemon Mansion 2F to Pokemon Mansion 3F", - multiworld.random.choice(mansion_stair_destinations + world.random.choice(mansion_stair_destinations + ["Pokemon Mansion B1F to Pokemon Mansion 1F-SE"]))) - if multiworld.door_shuffle[player] in ("insanity", "decoupled"): + if world.options.door_shuffle in ("insanity", "decoupled"): usable_safe_rooms += insanity_safe_rooms - safe_rooms_sample = multiworld.random.sample(usable_safe_rooms, 6) + safe_rooms_sample = world.random.sample(usable_safe_rooms, 6) pallet_safe_room = safe_rooms_sample[-1] - for a, b in zip(multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", + for a, b in zip(world.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", "Pallet Town to Rival's House"], 3), ["Oak's Lab to Pallet Town", "Player's House 1F to Pallet Town", pallet_safe_room]): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": for a, b in zip(["Oak's Lab to Pallet Town", "Player's House 1F to Pallet Town", pallet_safe_room], - multiworld.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", + world.random.sample(["Pallet Town to Player's House 1F", "Pallet Town to Oak's Lab", "Pallet Town to Rival's House"], 3)): one_way_forced_connections.add((a, b)) for a, b in zip(safari_zone_houses, safe_rooms_sample): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "decoupled": - for a, b in zip(multiworld.random.sample(safe_rooms_sample[:-1], len(safe_rooms_sample) - 1), + if world.options.door_shuffle == "decoupled": + for a, b in zip(world.random.sample(safe_rooms_sample[:-1], len(safe_rooms_sample) - 1), safari_zone_houses): one_way_forced_connections.add((a, b)) - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": # force Indigo Plateau Lobby to vanilla location on simple, otherwise shuffle with Pokemon Centers. - for a, b in zip(multiworld.random.sample(pokemon_center_entrances[0:-1], 11), pokemon_centers[0:-1]): + for a, b in zip(world.random.sample(pokemon_center_entrances[0:-1], 11), pokemon_centers[0:-1]): forced_connections.add((a, b)) forced_connections.add((pokemon_center_entrances[-1], pokemon_centers[-1])) - forced_pokemarts = multiworld.random.sample(pokemart_entrances, 8) - if multiworld.key_items_only[player]: + forced_pokemarts = world.random.sample(pokemart_entrances, 8) + if world.options.key_items_only: forced_pokemarts.sort(key=lambda i: i[0] != "Viridian Pokemart to Viridian City") for a, b in zip(forced_pokemarts, pokemarts): forced_connections.add((a, b)) @@ -2104,21 +2111,21 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): # fly / blackout warps. Rather than mess with those coordinates (besides in Pallet Town) or have players # warping outside an entrance that isn't the Pokemon Center, just always put Pokemon Centers at Pokemon # Center entrances - for a, b in zip(multiworld.random.sample(pokemon_center_entrances, 12), pokemon_centers): + for a, b in zip(world.random.sample(pokemon_center_entrances, 12), pokemon_centers): one_way_forced_connections.add((a, b)) # Ensure a Pokemart is available at the beginning of the game - if multiworld.key_items_only[player]: - one_way_forced_connections.add((multiworld.random.choice(initial_doors), + if world.options.key_items_only: + one_way_forced_connections.add((world.random.choice(initial_doors), "Viridian Pokemart to Viridian City")) elif "Pokemart" not in pallet_safe_room: - one_way_forced_connections.add((multiworld.random.choice(initial_doors), multiworld.random.choice( + one_way_forced_connections.add((world.random.choice(initial_doors), world.random.choice( [mart for mart in pokemarts if mart not in safe_rooms_sample]))) - if multiworld.warp_tile_shuffle[player] == "shuffle" or (multiworld.warp_tile_shuffle[player] == "mixed" - and multiworld.door_shuffle[player] - in ("off", "simple", "interiors")): - warps = multiworld.random.sample(silph_co_warps, len(silph_co_warps)) + if world.options.warp_tile_shuffle == "shuffle" or (world.options.warp_tile_shuffle == "mixed" + and world.options.door_shuffle + in ("off", "simple", "interiors")): + warps = world.random.sample(silph_co_warps, len(silph_co_warps)) # The only warp tiles never reachable from the stairs/elevators are the two 7F-NW warps (where the rival is) # and the final 11F-W warp. As long as the two 7F-NW warps aren't connected to each other, everything should # always be reachable. @@ -2129,9 +2136,9 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): # Shuffle Saffron Gym sections, then connect one warp from each section to the next. # Then connect the rest at random. - warps = multiworld.random.sample(saffron_gym_warps, len(saffron_gym_warps)) + warps = world.random.sample(saffron_gym_warps, len(saffron_gym_warps)) solution = ["SW", "W", "NW", "N", "NE", "E", "SE"] - multiworld.random.shuffle(solution) + world.random.shuffle(solution) solution = ["S"] + solution + ["C"] for i in range(len(solution) - 1): f, t = solution[i], solution[i + 1] @@ -2151,7 +2158,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): forced_connections.add((warps.pop(), warps.pop(),)) dc_destinations = None - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": dc_destinations = entrances.copy() for pair in one_way_forced_connections: entrance_a = multiworld.get_entrance(pair[0], player) @@ -2179,11 +2186,11 @@ def door_shuffle(world, multiworld, player, badges, badge_locs): full_interiors.remove(entrance_b) else: raise DoorShuffleException("Attempted to force connection with entrance not in any entrance pool, likely because it tried to force an entrance to connect twice.") - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": dc_destinations.remove(entrance_a) dc_destinations.remove(entrance_b) - if multiworld.door_shuffle[player] == "simple": + if world.options.door_shuffle == "simple": def connect_connecting_interiors(interior_exits, exterior_entrances): for interior, exterior in zip(interior_exits, exterior_entrances): for a, b in zip(interior, exterior): @@ -2222,68 +2229,68 @@ def connect_interiors(interior_exits, exterior_entrances): single_entrance_dungeon_entrances = dungeon_entrances.copy() for i in range(2): - if not multiworld.random.randint(0, 2): + if not world.random.randint(0, 2): placed_connecting_interior_dungeons.append(multi_purpose_dungeons[i]) interior_dungeon_entrances.append([multi_purpose_dungeon_entrances[i], None]) else: placed_single_entrance_dungeons.append(multi_purpose_dungeons[i]) single_entrance_dungeon_entrances.append(multi_purpose_dungeon_entrances[i]) - multiworld.random.shuffle(placed_connecting_interior_dungeons) + world.random.shuffle(placed_connecting_interior_dungeons) while placed_connecting_interior_dungeons[0] in unsafe_connecting_interior_dungeons: - multiworld.random.shuffle(placed_connecting_interior_dungeons) + world.random.shuffle(placed_connecting_interior_dungeons) connect_connecting_interiors(placed_connecting_interior_dungeons, interior_dungeon_entrances) interiors = connecting_interiors.copy() - multiworld.random.shuffle(interiors) + world.random.shuffle(interiors) while ((connecting_interiors[2] in (interiors[2], interiors[10], interiors[11]) # Dept Store at Dept Store # or Rt 16 Gate S or N and (interiors[11] in connecting_interiors[13:17] # Saffron Gate at Rt 16 Gate S or interiors[12] in connecting_interiors[13:17])) # Saffron Gate at Rt 18 Gate and interiors[15] in connecting_interiors[13:17] # Saffron Gate at Rt 7 Gate and interiors[1] in connecting_interiors[13:17] # Saffron Gate at Rt 7-8 Underground Path - and (not multiworld.tea[player]) and multiworld.worlds[player].fly_map != "Celadon City" - and multiworld.worlds[player].town_map_fly_map != "Celadon City"): - multiworld.random.shuffle(interiors) + and (not world.options.tea) and world.fly_map != "Celadon City" + and world.town_map_fly_map != "Celadon City"): + world.random.shuffle(interiors) connect_connecting_interiors(interiors, connecting_interior_entrances) placed_gyms = gyms.copy() - multiworld.random.shuffle(placed_gyms) + world.random.shuffle(placed_gyms) # Celadon Gym requires Cut access to reach the Gym Leader. There are some scenarios where its placement # could make badge placement impossible def celadon_gym_problem(): # Badgesanity or no badges needed for HM moves means gyms can go anywhere - if multiworld.badgesanity[player] or not multiworld.badges_needed_for_hm_moves[player]: + if world.options.badgesanity or not world.options.badges_needed_for_hm_moves: return False # Celadon Gym in Pewter City and need one or more badges for Viridian City gym. # No gym leaders would be reachable. - if gyms[3] == placed_gyms[0] and multiworld.viridian_gym_condition[player] > 0: + if gyms[3] == placed_gyms[0] and world.options.viridian_gym_condition > 0: return True # Celadon Gym not on Cinnabar Island or can access Viridian City gym with one badge - if not gyms[3] == placed_gyms[6] and multiworld.viridian_gym_condition[player] > 1: + if not gyms[3] == placed_gyms[6] and world.options.viridian_gym_condition > 1: return False # At this point we need to see if we can get beyond Pewter/Cinnabar with just one badge # Can get Fly access from Pewter City gym and fly beyond Pewter/Cinnabar - if multiworld.worlds[player].fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", - "Indigo Plateau") and multiworld.worlds[player].town_map_fly_map not in ("Pallet Town", + if world.fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", + "Indigo Plateau") and world.town_map_fly_map not in ("Pallet Town", "Viridian City", "Cinnabar Island", "Indigo Plateau"): return False # Route 3 condition is boulder badge but Mt Moon entrance leads to safe dungeons or Rock Tunnel - if multiworld.route_3_condition[player] == "boulder_badge" and placed_connecting_interior_dungeons[2] not \ + if world.options.route_3_condition == "boulder_badge" and placed_connecting_interior_dungeons[2] not \ in (unsafe_connecting_interior_dungeons[0], unsafe_connecting_interior_dungeons[2]): return False # Route 3 condition is Defeat Brock and he is in Pewter City, or any other condition besides Boulder Badge. # Any badge can land in Pewter City, so the only problematic dungeon at Mt Moon is Seafoam Islands since # it requires two badges - if (((multiworld.route_3_condition[player] == "defeat_brock" and gyms[0] == placed_gyms[0]) - or multiworld.route_3_condition[player] not in ("defeat_brock", "boulder_badge")) + if (((world.options.route_3_condition == "defeat_brock" and gyms[0] == placed_gyms[0]) + or world.options.route_3_condition not in ("defeat_brock", "boulder_badge")) and placed_connecting_interior_dungeons[2] != unsafe_connecting_interior_dungeons[0]): return False @@ -2305,31 +2312,31 @@ def cerulean_city_problem(): and interiors[0] in connecting_interiors[13:17] # Saffron Gate at Underground Path North South and interiors[13] in connecting_interiors[13:17] # Saffron Gate at Route 5 Saffron Gate and multi_purpose_dungeons[0] == placed_connecting_interior_dungeons[4] # Pokémon Mansion at Rock Tunnel, which is - and (not multiworld.tea[player]) # not traversable backwards - and multiworld.route_3_condition[player] == "defeat_brock" - and multiworld.worlds[player].fly_map != "Cerulean City" - and multiworld.worlds[player].town_map_fly_map != "Cerulean City"): + and (not world.options.tea) # not traversable backwards + and world.options.route_3_condition == "defeat_brock" + and world.fly_map != "Cerulean City" + and world.town_map_fly_map != "Cerulean City"): return True while celadon_gym_problem() or cerulean_city_problem(): - multiworld.random.shuffle(placed_gyms) + world.random.shuffle(placed_gyms) connect_interiors(placed_gyms, gym_entrances) - multiworld.random.shuffle(placed_single_entrance_dungeons) + world.random.shuffle(placed_single_entrance_dungeons) while dungeons[4] == placed_single_entrance_dungeons[0]: # Pokémon Tower at Silph Co - multiworld.random.shuffle(placed_single_entrance_dungeons) + world.random.shuffle(placed_single_entrance_dungeons) connect_interiors(placed_single_entrance_dungeons, single_entrance_dungeon_entrances) remaining_entrances = [entrance for entrance in entrances if outdoor_map(entrance.parent_region.name)] - multiworld.random.shuffle(remaining_entrances) + world.random.shuffle(remaining_entrances) remaining_interiors = [entrance for entrance in entrances if entrance not in remaining_entrances] for entrance_a, entrance_b in zip(remaining_entrances, remaining_interiors): entrance_a.connect(entrance_b) entrance_b.connect(entrance_a) - elif multiworld.door_shuffle[player]: - if multiworld.door_shuffle[player] == "full": - multiworld.random.shuffle(full_interiors) + elif world.options.door_shuffle: + if world.options.door_shuffle == "full": + world.random.shuffle(full_interiors) def search_for_exit(entrance, region, checked_regions): checked_regions.add(region) @@ -2344,6 +2351,7 @@ def search_for_exit(entrance, region, checked_regions): return found_exit return None + e = multiworld.get_entrance("Underground Path Route 5 to Underground Path North South", player) while True: for entrance_a in full_interiors: if search_for_exit(entrance_a, entrance_a.parent_region, set()) is None: @@ -2363,7 +2371,7 @@ def search_for_exit(entrance, region, checked_regions): break loop_out_interiors = [] - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) for entrance in reversed(entrances): if not outdoor_map(entrance.parent_region.name): found_exit = search_for_exit(entrance, entrance.parent_region, set()) @@ -2380,26 +2388,26 @@ def search_for_exit(entrance, region, checked_regions): entrance_a.connect(entrance_b) entrance_b.connect(entrance_a) - elif multiworld.door_shuffle[player] == "interiors": + elif world.options.door_shuffle == "interiors": loop_out_interiors = [[multiworld.get_entrance(e[0], player), multiworld.get_entrance(e[1], player)] for e - in multiworld.random.sample(unsafe_connecting_interior_dungeons - + safe_connecting_interior_dungeons, 2)] + in world.random.sample(unsafe_connecting_interior_dungeons + + safe_connecting_interior_dungeons, 2)] entrances.remove(loop_out_interiors[0][1]) entrances.remove(loop_out_interiors[1][1]) - if not multiworld.badgesanity[player]: - multiworld.random.shuffle(badges) - while badges[3].name == "Cascade Badge" and multiworld.badges_needed_for_hm_moves[player]: - multiworld.random.shuffle(badges) + if not world.options.badgesanity: + world.random.shuffle(badges) + while badges[3].name == "Cascade Badge" and world.options.badges_needed_for_hm_moves: + world.random.shuffle(badges) for badge, loc in zip(badges, badge_locs): loc.place_locked_item(badge) state = multiworld.state.copy() for item, data in item_table.items(): if (data.id or item in poke_data.pokemon_data) and data.classification == ItemClassification.progression \ - and ("Badge" not in item or multiworld.badgesanity[player]): + and ("Badge" not in item or world.options.badgesanity): state.collect(world.create_item(item)) - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) reachable_entrances = [] relevant_events = [ @@ -2415,13 +2423,13 @@ def search_for_exit(entrance, region, checked_regions): "Victory Road Boulder", "Silph Co Liberated", ] - if multiworld.robbed_house_officer[player]: + if world.options.robbed_house_officer: relevant_events.append("Help Bill") - if multiworld.tea[player]: + if world.options.tea: relevant_events.append("Vending Machine Drinks") - if multiworld.route_3_condition[player] == "defeat_brock": + if world.options.route_3_condition == "defeat_brock": relevant_events.append("Defeat Brock") - elif multiworld.route_3_condition[player] == "defeat_any_gym": + elif world.options.route_3_condition == "defeat_any_gym": relevant_events += [ "Defeat Brock", "Defeat Misty", @@ -2447,7 +2455,7 @@ def adds_reachable_entrances(item): def dead_end(e): if e.can_reach(state): return True - elif multiworld.door_shuffle[player] == "decoupled": + elif world.options.door_shuffle == "decoupled": # Any unreachable exit in decoupled is not a dead end return False region = e.parent_region @@ -2482,10 +2490,10 @@ def dead_end(e): state.update_reachable_regions(player) state.sweep_for_advancements(locations=event_locations) - multiworld.random.shuffle(entrances) + world.random.shuffle(entrances) - if multiworld.door_shuffle[player] == "decoupled": - multiworld.random.shuffle(dc_destinations) + if world.options.door_shuffle == "decoupled": + world.random.shuffle(dc_destinations) else: entrances.sort(key=lambda e: e.name not in entrance_only) @@ -2502,15 +2510,15 @@ def dead_end(e): is_outdoor_map = outdoor_map(entrance_a.parent_region.name) - if multiworld.door_shuffle[player] in ("interiors", "full") or len(entrances) != len(reachable_entrances): + if world.options.door_shuffle in ("interiors", "full") or len(entrances) != len(reachable_entrances): find_dead_end = False if (len(reachable_entrances) > - (1 if multiworld.door_shuffle[player] in ("insanity", "decoupled") else 8) and len(entrances) + (1 if world.options.door_shuffle in ("insanity", "decoupled") else 8) and len(entrances) <= (starting_entrances - 3)): find_dead_end = True - if (multiworld.door_shuffle[player] in ("interiors", "full") and len(entrances) < 48 + if (world.options.door_shuffle in ("interiors", "full") and len(entrances) < 48 and not is_outdoor_map): # Try to prevent a situation where the only remaining outdoor entrances are ones that cannot be # reached except by connecting directly to it. @@ -2519,9 +2527,9 @@ def dead_end(e): in reachable_entrances if not outdoor_map(entrance.parent_region.name)]) > 1: find_dead_end = True - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": destinations = dc_destinations - elif multiworld.door_shuffle[player] in ("interiors", "full"): + elif world.options.door_shuffle in ("interiors", "full"): destinations = [entrance for entrance in entrances if outdoor_map(entrance.parent_region.name) is not is_outdoor_map] if not destinations: @@ -2531,7 +2539,7 @@ def dead_end(e): destinations.sort(key=lambda e: e == entrance_a) for entrance in destinations: - if (dead_end(entrance) is find_dead_end and (multiworld.door_shuffle[player] != "decoupled" + if (dead_end(entrance) is find_dead_end and (world.options.door_shuffle != "decoupled" or entrance.parent_region.name.split("-")[0] != entrance_a.parent_region.name.split("-")[0])): entrance_b = entrance @@ -2540,28 +2548,28 @@ def dead_end(e): else: entrance_b = destinations.pop(0) - if multiworld.door_shuffle[player] in ("interiors", "full"): + if world.options.door_shuffle in ("interiors", "full"): # on Interiors/Full, the destinations variable does not point to the entrances list, so we need to # remove from that list here. entrances.remove(entrance_b) else: # Everything is reachable. Just start connecting the rest of the doors at random. - if multiworld.door_shuffle[player] == "decoupled": + if world.options.door_shuffle == "decoupled": entrance_b = dc_destinations.pop(0) else: entrance_b = entrances.pop(0) entrance_a.connect(entrance_b) - if multiworld.door_shuffle[player] != "decoupled": + if world.options.door_shuffle != "decoupled": entrance_b.connect(entrance_a) - if multiworld.door_shuffle[player] in ("interiors", "full"): + if world.options.door_shuffle in ("interiors", "full"): for pair in loop_out_interiors: pair[1].connected_region = pair[0].connected_region pair[1].parent_region.entrances.append(pair[0]) pair[1].target = pair[0].target - if multiworld.door_shuffle[player]: + if world.options.door_shuffle: for region in multiworld.get_regions(player): checked_regions = {region} @@ -2585,10 +2593,10 @@ def check_region(region_to_check): region.entrance_hint = check_region(region) -def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, +def connect(multiworld: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None): - source_region = world.get_region(source, player) - target_region = world.get_region(target, player) + source_region = multiworld.get_region(source, player) + target_region = multiworld.get_region(target, player) if name is None: name = source + " to " + target @@ -2604,7 +2612,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call source_region.exits.append(connection) connection.connect(target_region) if not one_way: - connect(world, player, target, source, rule, True) + connect(multiworld, player, target, source, rule, True) class PokemonRBWarp(Entrance): @@ -2621,7 +2629,7 @@ def access_rule(self, state): if self.connected_region is None: return False if "Elevator" in self.parent_region.name and ( - (state.multiworld.all_elevators_locked[self.player] + (state.multiworld.worlds[self.player].options.all_elevators_locked or "Rocket Hideout" in self.parent_region.name) and not state.has("Lift Key", self.player)): return False diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index b6c1221a29f4..5ebd204c9abc 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -13,22 +13,22 @@ from . import poke_data -def write_quizzes(self, data, random): +def write_quizzes(world, data, random): def get_quiz(q, a): if q == 0: r = random.randint(0, 3) if r == 0: - mon = self.trade_mons["Trade_Dux"] + mon = world.trade_mons["Trade_Dux"] text = "A woman inVermilion City" elif r == 1: - mon = self.trade_mons["Trade_Lola"] + mon = world.trade_mons["Trade_Lola"] text = "A man inCerulean City" elif r == 2: - mon = self.trade_mons["Trade_Marcel"] + mon = world.trade_mons["Trade_Marcel"] text = "Someone on Route 2" elif r == 3: - mon = self.trade_mons["Trade_Spot"] + mon = world.trade_mons["Trade_Spot"] text = "Someone on Route 5" if not a: answers.append(0) @@ -38,21 +38,30 @@ def get_quiz(q, a): return encode_text(f"{text}was looking for{mon}?") elif q == 1: - for location in self.multiworld.get_filled_locations(): - if location.item.name == "Secret Key" and location.item.player == self.player: + for location in world.multiworld.get_filled_locations(): + if location.item.name == "Secret Key" and location.item.player == world.player: break - player_name = self.multiworld.player_name[location.player] + player_name = world.multiworld.player_name[location.player] if not a: - if len(self.multiworld.player_name) > 1: + if len(world.multiworld.player_name) > 1: old_name = player_name while old_name == player_name: - player_name = random.choice(list(self.multiworld.player_name.values())) + player_name = random.choice(list(world.multiworld.player_name.values())) else: return encode_text("You're playingin a multiworldwith otherplayers?") - if player_name == self.multiworld.player_name[self.player]: - player_name = "yourself" - player_name = encode_text(player_name, force=True, safety=True) - return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("") + if world.multiworld.get_entrance( + "Cinnabar Island-G to Cinnabar Gym", world.player).connected_region.name == "Cinnabar Gym": + if player_name == world.multiworld.player_name[world.player]: + player_name = "yourself" + player_name = encode_text(player_name, force=True, safety=True) + return encode_text(f"The Secret Key wasfound by") + player_name + encode_text("?") + else: + # Might not have found it yet + if player_name == world.multiworld.player_name[world.player]: + return encode_text(f"The Secret Key wasplaced inyour own world?") + player_name = encode_text(player_name, force=True, safety=True) + return (encode_text(f"The Secret Key wasplaced in") + player_name + + encode_text("'sworld?")) elif q == 2: if a: return encode_text(f"#mon ispronouncedPo-kay-mon?") @@ -62,8 +71,8 @@ def get_quiz(q, a): else: return encode_text(f"#mon ispronouncedPo-kuh-mon?") elif q == 3: - starters = [" ".join(self.multiworld.get_location( - f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] + starters = [" ".join(world.multiworld.get_location( + f"Oak's Lab - Starter {i}", world.player).item.name.split(" ")[1:]) for i in range(1, 4)] mon = random.choice(starters) nots = random.choice(range(8, 16, 2)) if random.randint(0, 1): @@ -82,10 +91,10 @@ def get_quiz(q, a): return encode_text(text) elif q == 4: if a: - tm_text = self.local_tms[27] + tm_text = world.local_tms[27] else: - if self.multiworld.randomize_tm_moves[self.player]: - wrong_tms = self.local_tms.copy() + if world.options.randomize_tm_moves: + wrong_tms = world.local_tms.copy() wrong_tms.pop(27) tm_text = random.choice(wrong_tms) else: @@ -102,12 +111,36 @@ def get_quiz(q, a): i = random.randint(0, random.choice([9, 99])) return encode_text(f"POLIWAG evolves {i}times?") elif q == 7: - entity = "Motor Carrier" - if not a: - entity = random.choice(["Driver", "Shipper"]) - return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 states" - f"that the{entity}is responsiblefor planningroutes when" - "hazardousmaterials aretransported?") + q2 = random.randint(0, 2) + if q2 == 0: + entity = "Motor Carrier" + if not a: + entity = random.choice(["Driver", "Shipper"]) + return encode_text("Title 49 of theU.S. Code ofFederalRegulations part397.67 " + f"statesthat the{entity}is responsiblefor planning" + "routes whenhazardousmaterials aretransported?") + elif q2 == 1: + if a: + state = random.choice( + ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', + 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', + 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', + 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Jersey', 'New Mexico', + 'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', + 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', + 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']) + else: + state = "New Hampshire" + return encode_text( + f"As of 2024,{state}has a lawrequiring allfront seat vehicleoccupants to useseatbelts?") + elif q2 == 2: + if a: + country = random.choice(["The United States", "Mexico", "Canada", "Germany", "France", "China", + "Russia", "Spain", "Brazil", "Ukraine", "Saudi Arabia", "Egypt"]) + else: + country = random.choice(["The U.K.", "Pakistan", "India", "Japan", "Australia", + "New Zealand", "Thailand"]) + return encode_text(f"As of 2020,drivers in{country}drive on theright side ofthe road?") elif q == 8: mon = random.choice(list(poke_data.evolution_levels.keys())) level = poke_data.evolution_levels[mon] @@ -115,17 +148,17 @@ def get_quiz(q, a): level += random.choice(range(1, 6)) * random.choice((-1, 1)) return encode_text(f"{mon} evolvesat level {level}?") elif q == 9: - move = random.choice(list(self.local_move_data.keys())) - actual_type = self.local_move_data[move]["type"] + move = random.choice(list(world.local_move_data.keys())) + actual_type = world.local_move_data[move]["type"] question_type = actual_type while question_type == actual_type and not a: question_type = random.choice(list(poke_data.type_ids.keys())) return encode_text(f"{move} is{question_type} type?") elif q == 10: mon = random.choice(list(poke_data.pokemon_data.keys())) - actual_type = self.local_poke_data[mon][random.choice(("type1", "type2"))] + actual_type = world.local_poke_data[mon][random.choice(("type1", "type2"))] question_type = actual_type - while question_type in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]] and not a: + while question_type in [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]] and not a: question_type = random.choice(list(poke_data.type_ids.keys())) return encode_text(f"{mon} is{question_type} type?") elif q == 11: @@ -147,8 +180,8 @@ def get_quiz(q, a): return encode_text(f"{equation}= {question_result}?") elif q == 12: route = random.choice((12, 16)) - actual_mon = self.multiworld.get_location(f"Route {route} - Sleeping Pokemon", - self.player).item.name.split("Static ")[1] + actual_mon = world.multiworld.get_location(f"Route {route} - Sleeping Pokemon", + world.player).item.name.split("Static ")[1] question_mon = actual_mon while question_mon == actual_mon and not a: question_mon = random.choice(list(poke_data.pokemon_data.keys())) @@ -157,7 +190,7 @@ def get_quiz(q, a): type1 = random.choice(list(poke_data.type_ids.keys())) type2 = random.choice(list(poke_data.type_ids.keys())) eff_msgs = ["super effective", "no ", "not veryeffective", "normal "] - for matchup in self.type_chart: + for matchup in world.type_chart: if matchup[0] == type1 and matchup[1] == type2: if matchup[2] > 10: eff = eff_msgs[0] @@ -175,15 +208,25 @@ def get_quiz(q, a): eff = random.choice(eff_msgs) return encode_text(f"{type1} deals{eff}damage to{type2} type?") elif q == 14: - fossil_level = self.multiworld.get_location("Fossil Level - Trainer Parties", - self.player).party_data[0]['level'] + fossil_level = world.multiworld.get_location("Fossil Level - Trainer Parties", + world.player).party_data[0]['level'] if not a: fossil_level += random.choice((-5, 5)) return encode_text(f"Fossil #MONrevive at level{fossil_level}?") + elif q == 15: + if a: + fodmap = random.choice(["garlic", "onion", "milk", "watermelon", "cherries", "wheat", "barley", + "pistachios", "cashews", "kidney beans", "apples", "honey"]) + else: + fodmap = random.choice(["carrots", "potatoes", "oranges", "pineapple", "blueberries", "parmesan", + "eggs", "beef", "chicken", "oat", "rice", "maple syrup", "peanuts"]) + are_is = "are" if fodmap[-1] == "s" else "is" + return encode_text(f"According toMonash Uni.,{fodmap} {are_is}considered highin FODMAPs?") answers = [random.randint(0, 1) for _ in range(6)] - questions = random.sample((range(0, 15)), 6) + questions = random.sample((range(0, 16)), 6) + question_texts = [] for i, question in enumerate(questions): question_texts.append(get_quiz(question, answers[i])) @@ -193,9 +236,9 @@ def get_quiz(q, a): write_bytes(data, question_texts[i], rom_addresses[f"Text_Quiz_{quiz}"]) -def generate_output(self, output_directory: str): - random = self.multiworld.per_slot_randoms[self.player] - game_version = self.multiworld.game_version[self.player].current_key +def generate_output(world, output_directory: str): + random = world.random + game_version = world.options.game_version.current_key data = bytes(get_base_rom_bytes(game_version)) base_patch = pkgutil.get_data(__name__, f'basepatch_{game_version}.bsdiff4') @@ -205,8 +248,8 @@ def generate_output(self, output_directory: str): basemd5 = hashlib.md5() basemd5.update(data) - pallet_connections = {entrance: self.multiworld.get_entrance(f"Pallet Town to {entrance}", - self.player).connected_region.name for + pallet_connections = {entrance: world.multiworld.get_entrance(f"Pallet Town to {entrance}", + world.player).connected_region.name for entrance in ["Player's House 1F", "Oak's Lab", "Rival's House"]} paths = None @@ -222,11 +265,11 @@ def generate_output(self, output_directory: str): elif pallet_connections["Oak's Lab"] == "Player's House 1F": write_bytes(data, [0x5F, 0xC7, 0x0C, 0x0C, 0x00, 0x00], rom_addresses["Pallet_Fly_Coords"]) - for region in self.multiworld.get_regions(self.player): + for region in world.multiworld.get_regions(world.player): for entrance in region.exits: if isinstance(entrance, PokemonRBWarp): - self.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance", - self.player) + world.multiworld.spoiler.set_entrance(entrance.name, entrance.connected_region.name, "entrance", + world.player) warp_ids = (entrance.warp_id,) if isinstance(entrance.warp_id, int) else entrance.warp_id warp_to_ids = (entrance.target,) if isinstance(entrance.target, int) else entrance.target for i, warp_id in enumerate(warp_ids): @@ -241,32 +284,32 @@ def generate_output(self, output_directory: str): data[address] = 0 if "Elevator" in connected_map_name else warp_to_ids[i] data[address + 1] = map_ids[connected_map_name] - if self.multiworld.door_shuffle[self.player] == "simple": + if world.options.door_shuffle == "simple": for (entrance, _, _, map_coords_entries, map_name, _) in town_map_coords.values(): - destination = self.multiworld.get_entrance(entrance, self.player).connected_region.name + destination = world.multiworld.get_entrance(entrance, world.player).connected_region.name (_, x, y, _, _, map_order_entry) = town_map_coords[destination] for map_coord_entry in map_coords_entries: data[rom_addresses["Town_Map_Coords"] + (map_coord_entry * 4) + 1] = (y << 4) | x data[rom_addresses["Town_Map_Order"] + map_order_entry] = map_ids[map_name] - if not self.multiworld.key_items_only[self.player]: + if not world.options.key_items_only: for i, gym_leader in enumerate(("Pewter Gym - Brock TM", "Cerulean Gym - Misty TM", "Vermilion Gym - Lt. Surge TM", "Celadon Gym - Erika TM", "Fuchsia Gym - Koga TM", "Saffron Gym - Sabrina TM", "Cinnabar Gym - Blaine TM", "Viridian Gym - Giovanni TM")): - item_name = self.multiworld.get_location(gym_leader, self.player).item.name + item_name = world.multiworld.get_location(gym_leader, world.player).item.name if item_name.startswith("TM"): try: tm = int(item_name[2:4]) - move = poke_data.moves[self.local_tms[tm - 1]]["id"] + move = poke_data.moves[world.local_tms[tm - 1]]["id"] data[rom_addresses["Gym_Leader_Moves"] + (2 * i)] = move except KeyError: pass def set_trade_mon(address, loc): - mon = self.multiworld.get_location(loc, self.player).item.name + mon = world.multiworld.get_location(loc, world.player).item.name data[rom_addresses[address]] = poke_data.pokemon_data[mon]["id"] - self.trade_mons[address] = mon + world.trade_mons[address] = mon if game_version == "red": set_trade_mon("Trade_Terry", "Safari Zone Center - Wild Pokemon - 5") @@ -282,10 +325,10 @@ def set_trade_mon(address, loc): set_trade_mon("Trade_Doris", "Cerulean Cave 1F - Wild Pokemon - 9") set_trade_mon("Trade_Crinkles", "Route 12 - Wild Pokemon - 4") - data[rom_addresses['Fly_Location']] = self.fly_map_code - data[rom_addresses['Map_Fly_Location']] = self.town_map_fly_map_code + data[rom_addresses['Fly_Location']] = world.fly_map_code + data[rom_addresses['Map_Fly_Location']] = world.town_map_fly_map_code - if self.multiworld.fix_combat_bugs[self.player]: + if world.options.fix_combat_bugs: data[rom_addresses["Option_Fix_Combat_Bugs"]] = 1 data[rom_addresses["Option_Fix_Combat_Bugs_Focus_Energy"]] = 0x28 # jr z data[rom_addresses["Option_Fix_Combat_Bugs_HP_Drain_Dream_Eater"]] = 0x1A # ld a, (de) @@ -298,25 +341,25 @@ def set_trade_mon(address, loc): data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Effect"] + 1] = 5 # 5 bytes ahead data[rom_addresses["Option_Fix_Combat_Bugs_Heal_Stat_Modifiers"]] = 1 - if self.multiworld.poke_doll_skip[self.player] == "in_logic": + if world.options.poke_doll_skip == "in_logic": data[rom_addresses["Option_Silph_Scope_Skip"]] = 0x00 # nop data[rom_addresses["Option_Silph_Scope_Skip"] + 1] = 0x00 # nop data[rom_addresses["Option_Silph_Scope_Skip"] + 2] = 0x00 # nop - if self.multiworld.bicycle_gate_skips[self.player] == "patched": + if world.options.bicycle_gate_skips == "patched": data[rom_addresses["Option_Route_16_Gate_Fix"]] = 0x00 # nop data[rom_addresses["Option_Route_16_Gate_Fix"] + 1] = 0x00 # nop data[rom_addresses["Option_Route_18_Gate_Fix"]] = 0x00 # nop data[rom_addresses["Option_Route_18_Gate_Fix"] + 1] = 0x00 # nop - if self.multiworld.door_shuffle[self.player]: + if world.options.door_shuffle: data[rom_addresses["Entrance_Shuffle_Fuji_Warp"]] = 1 # prevent warping to Fuji's House from Pokemon Tower 7F - if self.multiworld.all_elevators_locked[self.player]: + if world.options.all_elevators_locked: data[rom_addresses["Option_Locked_Elevator_Celadon"]] = 0x20 # jr nz data[rom_addresses["Option_Locked_Elevator_Silph"]] = 0x20 # jr nz - if self.multiworld.tea[self.player].value: + if world.options.tea: data[rom_addresses["Option_Tea"]] = 1 data[rom_addresses["Guard_Drink_List"]] = 0x54 data[rom_addresses["Guard_Drink_List"] + 1] = 0 @@ -325,90 +368,94 @@ def set_trade_mon(address, loc): "Oh wait there,the road's closed."), rom_addresses["Text_Saffron_Gate"]) + data[rom_addresses["Tea_Key_Item_A"]] = 0x28 # jr .z + data[rom_addresses["Tea_Key_Item_B"]] = 0x28 # jr .z + data[rom_addresses["Tea_Key_Item_C"]] = 0x28 # jr .z + data[rom_addresses["Fossils_Needed_For_Second_Item"]] = ( - self.multiworld.second_fossil_check_condition[self.player].value) + world.options.second_fossil_check_condition.value) - data[rom_addresses["Option_Lose_Money"]] = int(not self.multiworld.lose_money_on_blackout[self.player].value) + data[rom_addresses["Option_Lose_Money"]] = int(not world.options.lose_money_on_blackout.value) - if self.multiworld.extra_key_items[self.player]: + if world.options.extra_key_items: data[rom_addresses['Option_Extra_Key_Items_A']] = 1 data[rom_addresses['Option_Extra_Key_Items_B']] = 1 data[rom_addresses['Option_Extra_Key_Items_C']] = 1 data[rom_addresses['Option_Extra_Key_Items_D']] = 1 - data[rom_addresses["Option_Split_Card_Key"]] = self.multiworld.split_card_key[self.player].value - data[rom_addresses["Option_Blind_Trainers"]] = round(self.multiworld.blind_trainers[self.player].value * 2.55) - data[rom_addresses["Option_Cerulean_Cave_Badges"]] = self.multiworld.cerulean_cave_badges_condition[self.player].value - data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = self.multiworld.cerulean_cave_key_items_condition[self.player].total - write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_badges_condition[self.player].value)), rom_addresses["Text_Cerulean_Cave_Badges"]) - write_bytes(data, encode_text(str(self.multiworld.cerulean_cave_key_items_condition[self.player].total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"]) - data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.multiworld.minimum_steps_between_encounters[self.player].value - data[rom_addresses['Option_Route23_Badges']] = self.multiworld.victory_road_condition[self.player].value - data[rom_addresses['Option_Victory_Road_Badges']] = self.multiworld.route_22_gate_condition[self.player].value - data[rom_addresses['Option_Elite_Four_Pokedex']] = self.multiworld.elite_four_pokedex_condition[self.player].total - data[rom_addresses['Option_Elite_Four_Key_Items']] = self.multiworld.elite_four_key_items_condition[self.player].total - data[rom_addresses['Option_Elite_Four_Badges']] = self.multiworld.elite_four_badges_condition[self.player].value - write_bytes(data, encode_text(str(self.multiworld.elite_four_badges_condition[self.player].value)), rom_addresses["Text_Elite_Four_Badges"]) - write_bytes(data, encode_text(str(self.multiworld.elite_four_key_items_condition[self.player].total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"]) - write_bytes(data, encode_text(str(self.multiworld.elite_four_pokedex_condition[self.player].total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"]) - write_bytes(data, encode_text(str(self.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"]) - - data[rom_addresses['Option_Viridian_Gym_Badges']] = self.multiworld.viridian_gym_condition[self.player].value - data[rom_addresses['Option_EXP_Modifier']] = self.multiworld.exp_modifier[self.player].value - if not self.multiworld.require_item_finder[self.player]: + data[rom_addresses["Option_Split_Card_Key"]] = world.options.split_card_key.value + data[rom_addresses["Option_Blind_Trainers"]] = round(world.options.blind_trainers.value * 2.55) + data[rom_addresses["Option_Cerulean_Cave_Badges"]] = world.options.cerulean_cave_badges_condition.value + data[rom_addresses["Option_Cerulean_Cave_Key_Items"]] = world.options.cerulean_cave_key_items_condition.total + write_bytes(data, encode_text(str(world.options.cerulean_cave_badges_condition.value)), rom_addresses["Text_Cerulean_Cave_Badges"]) + write_bytes(data, encode_text(str(world.options.cerulean_cave_key_items_condition.total) + " key items."), rom_addresses["Text_Cerulean_Cave_Key_Items"]) + data[rom_addresses['Option_Encounter_Minimum_Steps']] = world.options.minimum_steps_between_encounters.value + data[rom_addresses['Option_Route23_Badges']] = world.options.victory_road_condition.value + data[rom_addresses['Option_Victory_Road_Badges']] = world.options.route_22_gate_condition.value + data[rom_addresses['Option_Elite_Four_Pokedex']] = world.options.elite_four_pokedex_condition.total + data[rom_addresses['Option_Elite_Four_Key_Items']] = world.options.elite_four_key_items_condition.total + data[rom_addresses['Option_Elite_Four_Badges']] = world.options.elite_four_badges_condition.value + write_bytes(data, encode_text(str(world.options.elite_four_badges_condition.value)), rom_addresses["Text_Elite_Four_Badges"]) + write_bytes(data, encode_text(str(world.options.elite_four_key_items_condition.total) + " key items, and"), rom_addresses["Text_Elite_Four_Key_Items"]) + write_bytes(data, encode_text(str(world.options.elite_four_pokedex_condition.total) + " #MON"), rom_addresses["Text_Elite_Four_Pokedex"]) + write_bytes(data, encode_text(str(world.total_key_items), length=2), rom_addresses["Trainer_Screen_Total_Key_Items"]) + + data[rom_addresses['Option_Viridian_Gym_Badges']] = world.options.viridian_gym_condition.value + data[rom_addresses['Option_EXP_Modifier']] = world.options.exp_modifier.value + if not world.options.require_item_finder: data[rom_addresses['Option_Itemfinder']] = 0 # nop - if self.multiworld.extra_strength_boulders[self.player]: + if world.options.extra_strength_boulders: for i in range(0, 3): data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15 - if self.multiworld.extra_key_items[self.player]: + if world.options.extra_key_items: for i in range(0, 4): data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15 - if self.multiworld.old_man[self.player] == "open_viridian_city": + if world.options.old_man == "open_viridian_city": data[rom_addresses['Option_Old_Man']] = 0x11 data[rom_addresses['Option_Old_Man_Lying']] = 0x15 - data[rom_addresses['Option_Route3_Guard_B']] = self.multiworld.route_3_condition[self.player].value - if self.multiworld.route_3_condition[self.player] == "open": + data[rom_addresses['Option_Route3_Guard_B']] = world.options.route_3_condition.value + if world.options.route_3_condition == "open": data[rom_addresses['Option_Route3_Guard_A']] = 0x11 - if not self.multiworld.robbed_house_officer[self.player]: + if not world.options.robbed_house_officer: data[rom_addresses['Option_Trashed_House_Guard_A']] = 0x15 data[rom_addresses['Option_Trashed_House_Guard_B']] = 0x11 - if self.multiworld.require_pokedex[self.player]: + if world.options.require_pokedex: data[rom_addresses["Require_Pokedex_A"]] = 1 data[rom_addresses["Require_Pokedex_B"]] = 1 data[rom_addresses["Require_Pokedex_C"]] = 1 else: data[rom_addresses["Require_Pokedex_D"]] = 0x18 # jr - if self.multiworld.dexsanity[self.player]: + if world.options.dexsanity: data[rom_addresses["Option_Dexsanity_A"]] = 1 data[rom_addresses["Option_Dexsanity_B"]] = 1 - if self.multiworld.all_pokemon_seen[self.player]: + if world.options.all_pokemon_seen: data[rom_addresses["Option_Pokedex_Seen"]] = 1 - money = str(self.multiworld.starting_money[self.player].value).zfill(6) + money = str(world.options.starting_money.value).zfill(6) data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16) data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16) data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16) data[rom_addresses["Text_Badges_Needed_Viridian_Gym"]] = encode_text( - str(self.multiworld.viridian_gym_condition[self.player].value))[0] + str(world.options.viridian_gym_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_A"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_B"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_C"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Rt23_Badges_D"]] = encode_text( - str(self.multiworld.victory_road_condition[self.player].value))[0] + str(world.options.victory_road_condition.value))[0] data[rom_addresses["Text_Badges_Needed"]] = encode_text( - str(self.multiworld.elite_four_badges_condition[self.player].value))[0] + str(world.options.elite_four_badges_condition.value))[0] write_bytes(data, encode_text( - " ".join(self.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", self.player).item.name.upper().split()[1:])), + " ".join(world.multiworld.get_location("Route 4 Pokemon Center - Pokemon For Sale", world.player).item.name.upper().split()[1:])), rom_addresses["Text_Magikarp_Salesman"]) - if self.multiworld.badges_needed_for_hm_moves[self.player].value == 0: + if world.options.badges_needed_for_hm_moves.value == 0: for hm_move in poke_data.hm_moves: write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), rom_addresses["HM_" + hm_move + "_Badge_a"]) - elif self.extra_badges: + elif world.extra_badges: written_badges = {} - for hm_move, badge in self.extra_badges.items(): + for hm_move, badge in world.extra_badges.items(): data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F, "Thunder Badge": 0x57, "Rainbow Badge": 0x5F, "Soul Badge": 0x67, "Marsh Badge": 0x6F, @@ -427,7 +474,7 @@ def set_trade_mon(address, loc): write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")]) type_loc = rom_addresses["Type_Chart"] - for matchup in self.type_chart: + for matchup in world.type_chart: if matchup[2] != 10: # don't needlessly divide damage by 10 and multiply by 10 data[type_loc] = poke_data.type_ids[matchup[0]] data[type_loc + 1] = poke_data.type_ids[matchup[1]] @@ -437,52 +484,49 @@ def set_trade_mon(address, loc): data[type_loc + 1] = 0xFF data[type_loc + 2] = 0xFF - if self.multiworld.normalize_encounter_chances[self.player].value: + if world.options.normalize_encounter_chances.value: chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255] for i, chance in enumerate(chances): data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance - for mon, mon_data in self.local_poke_data.items(): + for mon, mon_data in world.local_poke_data.items(): if mon == "Mew": address = rom_addresses["Base_Stats_Mew"] else: address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1)) - data[address + 1] = self.local_poke_data[mon]["hp"] - data[address + 2] = self.local_poke_data[mon]["atk"] - data[address + 3] = self.local_poke_data[mon]["def"] - data[address + 4] = self.local_poke_data[mon]["spd"] - data[address + 5] = self.local_poke_data[mon]["spc"] - data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]] - data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]] - data[address + 8] = self.local_poke_data[mon]["catch rate"] - data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"] - data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"] - data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"] - data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"] - write_bytes(data, self.local_poke_data[mon]["tms"], address + 20) - if mon in self.learnsets and self.learnsets[mon]: + data[address + 1] = world.local_poke_data[mon]["hp"] + data[address + 2] = world.local_poke_data[mon]["atk"] + data[address + 3] = world.local_poke_data[mon]["def"] + data[address + 4] = world.local_poke_data[mon]["spd"] + data[address + 5] = world.local_poke_data[mon]["spc"] + data[address + 6] = poke_data.type_ids[world.local_poke_data[mon]["type1"]] + data[address + 7] = poke_data.type_ids[world.local_poke_data[mon]["type2"]] + data[address + 8] = world.local_poke_data[mon]["catch rate"] + data[address + 15] = poke_data.moves[world.local_poke_data[mon]["start move 1"]]["id"] + data[address + 16] = poke_data.moves[world.local_poke_data[mon]["start move 2"]]["id"] + data[address + 17] = poke_data.moves[world.local_poke_data[mon]["start move 3"]]["id"] + data[address + 18] = poke_data.moves[world.local_poke_data[mon]["start move 4"]]["id"] + write_bytes(data, world.local_poke_data[mon]["tms"], address + 20) + if mon in world.learnsets and world.learnsets[mon]: address = rom_addresses["Learnset_" + mon.replace(" ", "")] - for i, move in enumerate(self.learnsets[mon]): + for i, move in enumerate(world.learnsets[mon]): data[(address + 1) + i * 2] = poke_data.moves[move]["id"] - data[rom_addresses["Option_Aide_Rt2"]] = self.multiworld.oaks_aide_rt_2[self.player].value - data[rom_addresses["Option_Aide_Rt11"]] = self.multiworld.oaks_aide_rt_11[self.player].value - data[rom_addresses["Option_Aide_Rt15"]] = self.multiworld.oaks_aide_rt_15[self.player].value + data[rom_addresses["Option_Aide_Rt2"]] = world.options.oaks_aide_rt_2.value + data[rom_addresses["Option_Aide_Rt11"]] = world.options.oaks_aide_rt_11.value + data[rom_addresses["Option_Aide_Rt15"]] = world.options.oaks_aide_rt_15.value - if self.multiworld.safari_zone_normal_battles[self.player].value == 1: + if world.options.safari_zone_normal_battles.value == 1: data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255 - if self.multiworld.reusable_tms[self.player].value: + if world.options.reusable_tms.value: data[rom_addresses["Option_Reusable_TMs"]] = 0xC9 - for i in range(1, 10): - data[rom_addresses[f"Option_Trainersanity{i}"]] = self.multiworld.trainersanity[self.player].value - - data[rom_addresses["Option_Always_Half_STAB"]] = int(not self.multiworld.same_type_attack_bonus[self.player].value) + data[rom_addresses["Option_Always_Half_STAB"]] = int(not world.options.same_type_attack_bonus.value) - if self.multiworld.better_shops[self.player]: + if world.options.better_shops: inventory = ["Poke Ball", "Great Ball", "Ultra Ball"] - if self.multiworld.better_shops[self.player].value == 2: + if world.options.better_shops.value == 2: inventory.append("Master Ball") inventory += ["Potion", "Super Potion", "Hyper Potion", "Max Potion", "Full Restore", "Revive", "Antidote", "Awakening", "Burn Heal", "Ice Heal", "Paralyze Heal", "Full Heal", "Repel", "Super Repel", @@ -492,30 +536,30 @@ def set_trade_mon(address, loc): shop_data.append(0xFF) for shop in range(1, 11): write_bytes(data, shop_data, rom_addresses[f"Shop{shop}"]) - if self.multiworld.stonesanity[self.player]: + if world.options.stonesanity: write_bytes(data, bytearray([0xFE, 1, item_table["Poke Doll"].id - 172000000, 0xFF]), rom_addresses[f"Shop_Stones"]) - price = str(self.multiworld.master_ball_price[self.player].value).zfill(6) + price = str(world.options.master_ball_price.value).zfill(6) price = bytearray([int(price[:2], 16), int(price[2:4], 16), int(price[4:], 16)]) write_bytes(data, price, rom_addresses["Price_Master_Ball"]) # Money values in Red and Blue are weird - for item in reversed(self.multiworld.precollected_items[self.player]): + for item in reversed(world.multiworld.precollected_items[world.player]): if data[rom_addresses["Start_Inventory"] + item.code - 172000000] < 255: data[rom_addresses["Start_Inventory"] + item.code - 172000000] += 1 - set_mon_palettes(self, random, data) + set_mon_palettes(world, random, data) - for move_data in self.local_move_data.values(): + for move_data in world.local_move_data.values(): if move_data["id"] == 0: continue address = rom_addresses["Move_Data"] + ((move_data["id"] - 1) * 6) write_bytes(data, bytearray([move_data["id"], move_data["effect"], move_data["power"], poke_data.type_ids[move_data["type"]], round(move_data["accuracy"] * 2.55), move_data["pp"]]), address) - TM_IDs = bytearray([poke_data.moves[move]["id"] for move in self.local_tms]) + TM_IDs = bytearray([poke_data.moves[move]["id"] for move in world.local_tms]) write_bytes(data, TM_IDs, rom_addresses["TM_Moves"]) - if self.multiworld.randomize_rock_tunnel[self.player]: + if world.options.randomize_rock_tunnel: seed = randomize_rock_tunnel(data, random) write_bytes(data, encode_text(f"SEED: {seed}"), rom_addresses["Text_Rock_Tunnel_Sign"]) @@ -524,44 +568,44 @@ def set_trade_mon(address, loc): data[rom_addresses['Title_Mon_First']] = mons.pop() for mon in range(0, 16): data[rom_addresses['Title_Mons'] + mon] = mons.pop() - if self.multiworld.game_version[self.player].value: - mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name - else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name else - 2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3) + if world.options.game_version.value: + mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name + else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name else + 2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3) else: - mons.sort(key=lambda mon: 0 if mon == self.multiworld.get_location("Oak's Lab - Starter 2", self.player).item.name - else 1 if mon == self.multiworld.get_location("Oak's Lab - Starter 1", self.player).item.name else - 2 if mon == self.multiworld.get_location("Oak's Lab - Starter 3", self.player).item.name else 3) - write_bytes(data, encode_text(self.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed']) + mons.sort(key=lambda mon: 0 if mon == world.multiworld.get_location("Oak's Lab - Starter 2", world.player).item.name + else 1 if mon == world.multiworld.get_location("Oak's Lab - Starter 1", world.player).item.name else + 2 if mon == world.multiworld.get_location("Oak's Lab - Starter 3", world.player).item.name else 3) + write_bytes(data, encode_text(world.multiworld.seed_name[-20:], 20, True), rom_addresses['Title_Seed']) - slot_name = self.multiworld.player_name[self.player] + slot_name = world.multiworld.player_name[world.player] slot_name.replace("@", " ") slot_name.replace("<", " ") slot_name.replace(">", " ") write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name']) - if self.trainer_name == "choose_in_game": + if world.trainer_name == "choose_in_game": data[rom_addresses["Skip_Player_Name"]] = 0 else: - write_bytes(data, self.trainer_name, rom_addresses['Player_Name']) - if self.rival_name == "choose_in_game": + write_bytes(data, world.trainer_name, rom_addresses['Player_Name']) + if world.rival_name == "choose_in_game": data[rom_addresses["Skip_Rival_Name"]] = 0 else: - write_bytes(data, self.rival_name, rom_addresses['Rival_Name']) + write_bytes(data, world.rival_name, rom_addresses['Rival_Name']) data[0xFF00] = 2 # client compatibility version - rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0', + rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] rom_name.extend([0] * (21 - len(rom_name))) write_bytes(data, rom_name, 0xFFC6) - write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB) - write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0) + write_bytes(data, world.multiworld.seed_name.encode(), 0xFFDB) + write_bytes(data, world.multiworld.player_name[world.player].encode(), 0xFFF0) - self.finished_level_scaling.wait() + world.finished_level_scaling.wait() - write_quizzes(self, data, random) + write_quizzes(world, data, random) - for location in self.multiworld.get_locations(self.player): + for location in world.multiworld.get_locations(world.player): if location.party_data: for party in location.party_data: if not isinstance(party["party_address"], list): @@ -588,7 +632,7 @@ def set_trade_mon(address, loc): continue elif location.rom_address is None: continue - if location.item and location.item.player == self.player: + if location.item and location.item.player == world.player: if location.rom_address: rom_address = location.rom_address if not isinstance(rom_address, list): @@ -599,7 +643,7 @@ def set_trade_mon(address, loc): elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys(): data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"] else: - item_id = self.item_name_to_id[location.item.name] - 172000000 + item_id = world.item_name_to_id[location.item.name] - 172000000 if item_id > 255: item_id -= 256 data[address] = item_id @@ -613,18 +657,18 @@ def set_trade_mon(address, loc): for address in rom_address: data[address] = 0x2C # AP Item - outfilepname = f'_P{self.player}' - outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" \ - if self.multiworld.player_name[self.player] != 'Player%d' % self.player else '' - rompath = os.path.join(output_directory, f'AP_{self.multiworld.seed_name}{outfilepname}.gb') + outfilepname = f'_P{world.player}' + outfilepname += f"_{world.multiworld.get_file_safe_player_name(world.player).replace(' ', '_')}" \ + if world.multiworld.player_name[world.player] != 'Player%d' % world.player else '' + rompath = os.path.join(output_directory, f'AP_{world.multiworld.seed_name}{outfilepname}.gb') with open(rompath, 'wb') as outfile: outfile.write(data) - if self.multiworld.game_version[self.player].current_key == "red": - patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rompath) + if world.options.game_version.current_key == "red": + patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=world.player, + player_name=world.multiworld.player_name[world.player], patched_path=rompath) else: - patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=rompath) + patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=world.player, + player_name=world.multiworld.player_name[world.player], patched_path=rompath) patch.write() os.unlink(rompath) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index e5c073971d5d..ec233d94d44d 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -1,10 +1,9 @@ rom_addresses = { "Option_Encounter_Minimum_Steps": 0x3c1, "Option_Pitch_Black_Rock_Tunnel": 0x76a, - "Option_Blind_Trainers": 0x30d5, - "Option_Trainersanity1": 0x3165, - "Option_Split_Card_Key": 0x3e1e, - "Option_Fix_Combat_Bugs": 0x3e1f, + "Option_Blind_Trainers": 0x32f0, + "Option_Split_Card_Key": 0x3e19, + "Option_Fix_Combat_Bugs": 0x3e1a, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, @@ -115,9 +114,10 @@ "HM_Strength_Badge_b": 0x131ed, "HM_Flash_Badge_a": 0x131fc, "HM_Flash_Badge_b": 0x13201, - "Trainer_Screen_Total_Key_Items": 0x135dc, - "TM_Moves": 0x137b1, - "Encounter_Chances": 0x13950, + "Tea_Key_Item_A": 0x135ac, + "Trainer_Screen_Total_Key_Items": 0x1361b, + "TM_Moves": 0x137f0, + "Encounter_Chances": 0x1398f, "Warps_CeladonCity": 0x18026, "Warps_PalletTown": 0x182c7, "Warps_ViridianCity": 0x18388, @@ -128,52 +128,54 @@ "Option_Viridian_Gym_Badges": 0x1901d, "Event_Sleepy_Guy": 0x191d1, "Option_Route3_Guard_B": 0x1928a, - "Starter2_K": 0x19611, - "Starter3_K": 0x19619, - "Event_Rocket_Thief": 0x19733, - "Option_Cerulean_Cave_Badges": 0x19861, - "Option_Cerulean_Cave_Key_Items": 0x19868, - "Text_Cerulean_Cave_Badges": 0x198d7, - "Text_Cerulean_Cave_Key_Items": 0x198e5, - "Event_Stranded_Man": 0x19b3c, - "Event_Rivals_Sister": 0x19d0f, - "Warps_BluesHouse": 0x19d65, - "Warps_VermilionTradeHouse": 0x19dbc, - "Require_Pokedex_D": 0x19e53, - "Option_Elite_Four_Key_Items": 0x19e9d, - "Option_Elite_Four_Pokedex": 0x19ea4, - "Option_Elite_Four_Badges": 0x19eab, - "Text_Elite_Four_Badges": 0x19f47, - "Text_Elite_Four_Key_Items": 0x19f51, - "Text_Elite_Four_Pokedex": 0x19f64, - "Shop10": 0x1a018, - "Warps_IndigoPlateauLobby": 0x1a044, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a16c, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a17a, - "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a188, - "Event_SKC4F": 0x1a19b, - "Warps_SilphCo4F": 0x1a21d, - "Missable_Silph_Co_4F_Item_1": 0x1a25d, - "Missable_Silph_Co_4F_Item_2": 0x1a264, - "Missable_Silph_Co_4F_Item_3": 0x1a26b, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a3c3, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a3d1, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a3df, - "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a3ed, - "Event_SKC5F": 0x1a400, - "Warps_SilphCo5F": 0x1a4aa, - "Missable_Silph_Co_5F_Item_1": 0x1a4f2, - "Missable_Silph_Co_5F_Item_2": 0x1a4f9, - "Missable_Silph_Co_5F_Item_3": 0x1a500, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a630, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a63e, - "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a64c, - "Event_SKC6F": 0x1a66d, - "Warps_SilphCo6F": 0x1a74b, - "Missable_Silph_Co_6F_Item_1": 0x1a79b, - "Missable_Silph_Co_6F_Item_2": 0x1a7a2, - "Path_Pallet_Oak": 0x1a928, - "Path_Pallet_Player": 0x1a935, + "Starter2_K": 0x19618, + "Starter3_K": 0x19620, + "Event_Rocket_Thief": 0x1973a, + "Tea_Key_Item_C": 0x1988f, + "Option_Cerulean_Cave_Badges": 0x198a0, + "Option_Cerulean_Cave_Key_Items": 0x198a7, + "Text_Cerulean_Cave_Badges": 0x19916, + "Text_Cerulean_Cave_Key_Items": 0x19924, + "Event_Stranded_Man": 0x19b7b, + "Event_Rivals_Sister": 0x19d4e, + "Warps_BluesHouse": 0x19da4, + "Warps_VermilionTradeHouse": 0x19dfb, + "Require_Pokedex_D": 0x19e99, + "Tea_Key_Item_B": 0x19f13, + "Option_Elite_Four_Key_Items": 0x19f1b, + "Option_Elite_Four_Pokedex": 0x19f22, + "Option_Elite_Four_Badges": 0x19f29, + "Text_Elite_Four_Badges": 0x19fc5, + "Text_Elite_Four_Key_Items": 0x19fcf, + "Text_Elite_Four_Pokedex": 0x19fe2, + "Shop10": 0x1a096, + "Warps_IndigoPlateauLobby": 0x1a0c2, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_0_ITEM": 0x1a1ea, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_1_ITEM": 0x1a1f8, + "Trainersanity_EVENT_BEAT_SILPH_CO_4F_TRAINER_2_ITEM": 0x1a206, + "Event_SKC4F": 0x1a219, + "Warps_SilphCo4F": 0x1a29b, + "Missable_Silph_Co_4F_Item_1": 0x1a2db, + "Missable_Silph_Co_4F_Item_2": 0x1a2e2, + "Missable_Silph_Co_4F_Item_3": 0x1a2e9, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_0_ITEM": 0x1a441, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_1_ITEM": 0x1a44f, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_2_ITEM": 0x1a45d, + "Trainersanity_EVENT_BEAT_SILPH_CO_5F_TRAINER_3_ITEM": 0x1a46b, + "Event_SKC5F": 0x1a47e, + "Warps_SilphCo5F": 0x1a528, + "Missable_Silph_Co_5F_Item_1": 0x1a570, + "Missable_Silph_Co_5F_Item_2": 0x1a577, + "Missable_Silph_Co_5F_Item_3": 0x1a57e, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a6ae, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a6bc, + "Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a6ca, + "Event_SKC6F": 0x1a6eb, + "Warps_SilphCo6F": 0x1a7c9, + "Missable_Silph_Co_6F_Item_1": 0x1a819, + "Missable_Silph_Co_6F_Item_2": 0x1a820, + "Path_Pallet_Oak": 0x1a9a6, + "Path_Pallet_Player": 0x1a9b3, "Warps_CinnabarIsland": 0x1c026, "Warps_Route1": 0x1c0e9, "Option_Extra_Key_Items_B": 0x1ca46, @@ -191,75 +193,75 @@ "Starter2_E": 0x1d2f7, "Starter3_E": 0x1d2ff, "Event_Pokedex": 0x1d363, - "Event_Oaks_Gift": 0x1d393, - "Starter2_P": 0x1d481, - "Starter3_P": 0x1d489, - "Warps_OaksLab": 0x1d6af, - "Event_Pokemart_Quest": 0x1d76b, - "Shop1": 0x1d795, - "Warps_ViridianMart": 0x1d7d8, - "Warps_ViridianSchoolHouse": 0x1d82b, - "Warps_ViridianNicknameHouse": 0x1d889, - "Warps_PewterNidoranHouse": 0x1d8e4, - "Warps_PewterSpeechHouse": 0x1d927, - "Warps_CeruleanTrashedHouse": 0x1d98d, - "Warps_CeruleanTradeHouse": 0x1d9de, - "Event_Bicycle_Shop": 0x1da2f, - "Bike_Shop_Item_Display": 0x1da8a, - "Warps_BikeShop": 0x1db45, - "Event_Fuji": 0x1dbfd, - "Warps_MrFujisHouse": 0x1dc44, - "Warps_LavenderCuboneHouse": 0x1dcc0, - "Warps_NameRatersHouse": 0x1ddae, - "Warps_VermilionPidgeyHouse": 0x1ddf8, - "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de4e, - "Warps_VermilionDock": 0x1de70, - "Static_Encounter_Mew": 0x1de7e, - "Gift_Eevee": 0x1def7, - "Warps_CeladonMansionRoofHouse": 0x1df0e, - "Shop7": 0x1df49, - "Warps_FuchsiaMart": 0x1df74, - "Warps_SaffronPidgeyHouse": 0x1dfdd, - "Event_Mr_Psychic": 0x1e020, - "Warps_MrPsychicsHouse": 0x1e05d, - "Warps_DiglettsCaveRoute2": 0x1e092, - "Warps_Route2TradeHouse": 0x1e0da, - "Warps_Route5Gate": 0x1e1db, - "Warps_Route6Gate": 0x1e2ad, - "Warps_Route7Gate": 0x1e383, - "Warps_Route8Gate": 0x1e454, - "Warps_UndergroundPathRoute8": 0x1e4a5, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e511, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e51f, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e52d, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e53b, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e549, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e557, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e565, - "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e573, - "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e581, - "Warps_PowerPlant": 0x1e5de, - "Static_Encounter_Voltorb_A": 0x1e5f0, - "Static_Encounter_Voltorb_B": 0x1e5f8, - "Static_Encounter_Voltorb_C": 0x1e600, - "Static_Encounter_Electrode_A": 0x1e608, - "Static_Encounter_Voltorb_D": 0x1e610, - "Static_Encounter_Voltorb_E": 0x1e618, - "Static_Encounter_Electrode_B": 0x1e620, - "Static_Encounter_Voltorb_F": 0x1e628, - "Static_Encounter_Zapdos": 0x1e630, - "Missable_Power_Plant_Item_1": 0x1e638, - "Missable_Power_Plant_Item_2": 0x1e63f, - "Missable_Power_Plant_Item_3": 0x1e646, - "Missable_Power_Plant_Item_4": 0x1e64d, - "Missable_Power_Plant_Item_5": 0x1e654, - "Warps_DiglettsCaveRoute11": 0x1e7e9, - "Event_Rt16_House_Woman": 0x1e827, - "Warps_Route16FlyHouse": 0x1e870, - "Option_Victory_Road_Badges": 0x1e8f3, - "Warps_Route22Gate": 0x1e9e3, - "Event_Bill": 0x1eb24, - "Warps_BillsHouse": 0x1eb83, + "Event_Oaks_Gift": 0x1d398, + "Starter2_P": 0x1d486, + "Starter3_P": 0x1d48e, + "Warps_OaksLab": 0x1d6b4, + "Event_Pokemart_Quest": 0x1d770, + "Shop1": 0x1d79a, + "Warps_ViridianMart": 0x1d7dd, + "Warps_ViridianSchoolHouse": 0x1d830, + "Warps_ViridianNicknameHouse": 0x1d88e, + "Warps_PewterNidoranHouse": 0x1d8e9, + "Warps_PewterSpeechHouse": 0x1d92c, + "Warps_CeruleanTrashedHouse": 0x1d992, + "Warps_CeruleanTradeHouse": 0x1d9e3, + "Event_Bicycle_Shop": 0x1da34, + "Bike_Shop_Item_Display": 0x1da8f, + "Warps_BikeShop": 0x1db4a, + "Event_Fuji": 0x1dc02, + "Warps_MrFujisHouse": 0x1dc49, + "Warps_LavenderCuboneHouse": 0x1dcc5, + "Warps_NameRatersHouse": 0x1ddb3, + "Warps_VermilionPidgeyHouse": 0x1ddfd, + "Trainersanity_EVENT_BEAT_MEW_ITEM": 0x1de53, + "Warps_VermilionDock": 0x1de75, + "Static_Encounter_Mew": 0x1de83, + "Gift_Eevee": 0x1defc, + "Warps_CeladonMansionRoofHouse": 0x1df13, + "Shop7": 0x1df4e, + "Warps_FuchsiaMart": 0x1df79, + "Warps_SaffronPidgeyHouse": 0x1dfe2, + "Event_Mr_Psychic": 0x1e025, + "Warps_MrPsychicsHouse": 0x1e062, + "Warps_DiglettsCaveRoute2": 0x1e097, + "Warps_Route2TradeHouse": 0x1e0df, + "Warps_Route5Gate": 0x1e1e0, + "Warps_Route6Gate": 0x1e2b2, + "Warps_Route7Gate": 0x1e388, + "Warps_Route8Gate": 0x1e459, + "Warps_UndergroundPathRoute8": 0x1e4aa, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_0_ITEM": 0x1e516, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_1_ITEM": 0x1e524, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_2_ITEM": 0x1e532, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_3_ITEM": 0x1e540, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_4_ITEM": 0x1e54e, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_5_ITEM": 0x1e55c, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_6_ITEM": 0x1e56a, + "Trainersanity_EVENT_BEAT_POWER_PLANT_VOLTORB_7_ITEM": 0x1e578, + "Trainersanity_EVENT_BEAT_ZAPDOS_ITEM": 0x1e586, + "Warps_PowerPlant": 0x1e5e3, + "Static_Encounter_Voltorb_A": 0x1e5f5, + "Static_Encounter_Voltorb_B": 0x1e5fd, + "Static_Encounter_Voltorb_C": 0x1e605, + "Static_Encounter_Electrode_A": 0x1e60d, + "Static_Encounter_Voltorb_D": 0x1e615, + "Static_Encounter_Voltorb_E": 0x1e61d, + "Static_Encounter_Electrode_B": 0x1e625, + "Static_Encounter_Voltorb_F": 0x1e62d, + "Static_Encounter_Zapdos": 0x1e635, + "Missable_Power_Plant_Item_1": 0x1e63d, + "Missable_Power_Plant_Item_2": 0x1e644, + "Missable_Power_Plant_Item_3": 0x1e64b, + "Missable_Power_Plant_Item_4": 0x1e652, + "Missable_Power_Plant_Item_5": 0x1e659, + "Warps_DiglettsCaveRoute11": 0x1e7ee, + "Event_Rt16_House_Woman": 0x1e82c, + "Warps_Route16FlyHouse": 0x1e875, + "Option_Victory_Road_Badges": 0x1e8f8, + "Warps_Route22Gate": 0x1e9e8, + "Event_Bill": 0x1eb29, + "Warps_BillsHouse": 0x1eb88, "Starter1_O": 0x372b0, "Starter2_O": 0x372b4, "Starter3_O": 0x372b8, @@ -1470,74 +1472,73 @@ "Trainersanity_EVENT_BEAT_POKEMONTOWER_5_TRAINER_3_ITEM": 0x609ea, "Warps_PokemonTower5F": 0x60a5e, "Missable_Pokemon_Tower_5F_Item": 0x60a92, - "Option_Trainersanity2": 0x60b2a, - "Ghost_Battle1": 0x60b83, - "Ghost_Battle_Level": 0x60b88, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c25, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c33, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c41, - "Ghost_Battle2": 0x60c69, - "Warps_PokemonTower6F": 0x60cbe, - "Missable_Pokemon_Tower_6F_Item_1": 0x60ce4, - "Missable_Pokemon_Tower_6F_Item_2": 0x60ceb, - "Entrance_Shuffle_Fuji_Warp": 0x60deb, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60edf, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60eed, - "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60efb, - "Warps_PokemonTower7F": 0x60f8b, - "Warps_CeladonMart1F": 0x61033, - "Gift_Aerodactyl": 0x610f5, - "Gift_Omanyte": 0x610f9, - "Gift_Kabuto": 0x610fd, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x611de, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x611ec, - "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x611fa, - "Warps_ViridianForest": 0x61273, - "Missable_Viridian_Forest_Item_1": 0x612c1, - "Missable_Viridian_Forest_Item_2": 0x612c8, - "Missable_Viridian_Forest_Item_3": 0x612cf, - "Warps_SSAnne1F": 0x61310, - "Starter2_M": 0x614e5, - "Starter3_M": 0x614ed, - "Warps_SSAnne2F": 0x615ab, - "Warps_SSAnneB1F": 0x616c9, - "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x61771, - "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x6177f, - "Warps_SSAnneBow": 0x617c6, - "Warps_SSAnneKitchen": 0x618b6, - "Event_SS_Anne_Captain": 0x6194e, - "Warps_SSAnneCaptainsRoom": 0x619d5, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a3d, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a4b, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a59, - "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a67, - "Warps_SSAnne1FRooms": 0x61af7, - "Missable_SS_Anne_1F_Item": 0x61b53, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c24, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c32, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c40, - "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c4e, - "Warps_SSAnne2FRooms": 0x61d2c, - "Missable_SS_Anne_2F_Item_1": 0x61d88, - "Missable_SS_Anne_2F_Item_2": 0x61d9b, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e2c, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e3a, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e48, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e56, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e64, - "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e72, - "Warps_SSAnneB1FRooms": 0x61f20, - "Missable_SS_Anne_B1F_Item_1": 0x61f8a, - "Missable_SS_Anne_B1F_Item_2": 0x61f91, - "Missable_SS_Anne_B1F_Item_3": 0x61f98, - "Warps_UndergroundPathNorthSouth": 0x61fd5, - "Warps_UndergroundPathWestEast": 0x61ff9, - "Warps_DiglettsCave": 0x6201d, - "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62358, - "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62366, - "Event_Silph_Co_President": 0x62373, - "Event_SKC11F": 0x623bd, - "Warps_SilphCo11F": 0x62446, + "Ghost_Battle1": 0x60b93, + "Ghost_Battle_Level": 0x60b98, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_0_ITEM": 0x60c35, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_1_ITEM": 0x60c43, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_6_TRAINER_2_ITEM": 0x60c51, + "Ghost_Battle2": 0x60c79, + "Warps_PokemonTower6F": 0x60cce, + "Missable_Pokemon_Tower_6F_Item_1": 0x60cf4, + "Missable_Pokemon_Tower_6F_Item_2": 0x60cfb, + "Entrance_Shuffle_Fuji_Warp": 0x60dfb, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_0_ITEM": 0x60eef, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_1_ITEM": 0x60efd, + "Trainersanity_EVENT_BEAT_POKEMONTOWER_7_TRAINER_2_ITEM": 0x60f0b, + "Warps_PokemonTower7F": 0x60f9b, + "Warps_CeladonMart1F": 0x61043, + "Gift_Aerodactyl": 0x61105, + "Gift_Omanyte": 0x61109, + "Gift_Kabuto": 0x6110d, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_0_ITEM": 0x61209, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_1_ITEM": 0x61217, + "Trainersanity_EVENT_BEAT_VIRIDIAN_FOREST_TRAINER_2_ITEM": 0x61225, + "Warps_ViridianForest": 0x6129e, + "Missable_Viridian_Forest_Item_1": 0x612ec, + "Missable_Viridian_Forest_Item_2": 0x612f3, + "Missable_Viridian_Forest_Item_3": 0x612fa, + "Warps_SSAnne1F": 0x6133b, + "Starter2_M": 0x61510, + "Starter3_M": 0x61518, + "Warps_SSAnne2F": 0x615d6, + "Warps_SSAnneB1F": 0x616f4, + "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_0_ITEM": 0x6179c, + "Trainersanity_EVENT_BEAT_SS_ANNE_5_TRAINER_1_ITEM": 0x617aa, + "Warps_SSAnneBow": 0x617f1, + "Warps_SSAnneKitchen": 0x618e1, + "Event_SS_Anne_Captain": 0x61979, + "Warps_SSAnneCaptainsRoom": 0x61a00, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_0_ITEM": 0x61a68, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_1_ITEM": 0x61a76, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_2_ITEM": 0x61a84, + "Trainersanity_EVENT_BEAT_SS_ANNE_8_TRAINER_3_ITEM": 0x61a92, + "Warps_SSAnne1FRooms": 0x61b22, + "Missable_SS_Anne_1F_Item": 0x61b7e, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_0_ITEM": 0x61c4f, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_1_ITEM": 0x61c5d, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_2_ITEM": 0x61c6b, + "Trainersanity_EVENT_BEAT_SS_ANNE_9_TRAINER_3_ITEM": 0x61c79, + "Warps_SSAnne2FRooms": 0x61d57, + "Missable_SS_Anne_2F_Item_1": 0x61db3, + "Missable_SS_Anne_2F_Item_2": 0x61dc6, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_0_ITEM": 0x61e57, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_1_ITEM": 0x61e65, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_2_ITEM": 0x61e73, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_3_ITEM": 0x61e81, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_4_ITEM": 0x61e8f, + "Trainersanity_EVENT_BEAT_SS_ANNE_10_TRAINER_5_ITEM": 0x61e9d, + "Warps_SSAnneB1FRooms": 0x61f4b, + "Missable_SS_Anne_B1F_Item_1": 0x61fb5, + "Missable_SS_Anne_B1F_Item_2": 0x61fbc, + "Missable_SS_Anne_B1F_Item_3": 0x61fc3, + "Warps_UndergroundPathNorthSouth": 0x62000, + "Warps_UndergroundPathWestEast": 0x62024, + "Warps_DiglettsCave": 0x62048, + "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_0_ITEM": 0x62383, + "Trainersanity_EVENT_BEAT_SILPH_CO_11F_TRAINER_1_ITEM": 0x62391, + "Event_Silph_Co_President": 0x6239e, + "Event_SKC11F": 0x623e8, + "Warps_SilphCo11F": 0x62471, "Ghost_Battle4": 0x708e1, "Town_Map_Order": 0x70f0f, "Town_Map_Coords": 0x71381, @@ -1589,44 +1590,37 @@ "Warps_FuchsiaMeetingRoom": 0x75879, "Badge_Cinnabar_Gym": 0x759de, "Event_Cinnabar_Gym": 0x759f2, - "Option_Trainersanity4": 0x75ace, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75ada, - "Option_Trainersanity3": 0x75b1e, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2a, - "Option_Trainersanity5": 0x75b85, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b91, - "Option_Trainersanity6": 0x75bd5, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be1, - "Option_Trainersanity7": 0x75c25, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c31, - "Option_Trainersanity8": 0x75c75, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c81, - "Option_Trainersanity9": 0x75cc5, - "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cd1, - "Warps_CinnabarGym": 0x75d1b, - "Warps_CinnabarLab": 0x75e02, - "Warps_CinnabarLabTradeRoom": 0x75e94, - "Event_Lab_Scientist": 0x75ee9, - "Warps_CinnabarLabMetronomeRoom": 0x75f35, - "Fossils_Needed_For_Second_Item": 0x75fb6, - "Fossil_Level": 0x76017, - "Event_Dome_Fossil_B": 0x76031, - "Event_Helix_Fossil_B": 0x76051, - "Warps_CinnabarLabFossilRoom": 0x760d2, - "Warps_CinnabarPokecenter": 0x76128, - "Shop8": 0x7616f, - "Warps_CinnabarMart": 0x7619b, - "Warps_CopycatsHouse1F": 0x761ed, - "Starter2_N": 0x762a2, - "Starter3_N": 0x762aa, - "Warps_ChampionsRoom": 0x764d5, - "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76604, - "Warps_LoreleisRoom": 0x76628, - "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7675d, - "Warps_BrunosRoom": 0x76781, - "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768bc, - "Warps_AgathasRoom": 0x768e0, - "Option_Itemfinder": 0x76a33, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_B_ITEM": 0x75adc, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_A_ITEM": 0x75b2e, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_2_ITEM": 0x75b97, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_3_ITEM": 0x75be9, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_4_ITEM": 0x75c3b, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_5_ITEM": 0x75c8d, + "Trainersanity_EVENT_BEAT_CINNABAR_GYM_TRAINER_6_ITEM": 0x75cdf, + "Warps_CinnabarGym": 0x75d29, + "Warps_CinnabarLab": 0x75e10, + "Warps_CinnabarLabTradeRoom": 0x75ea2, + "Event_Lab_Scientist": 0x75ef7, + "Warps_CinnabarLabMetronomeRoom": 0x75f43, + "Fossils_Needed_For_Second_Item": 0x75fc4, + "Fossil_Level": 0x76025, + "Event_Dome_Fossil_B": 0x7603f, + "Event_Helix_Fossil_B": 0x7605f, + "Warps_CinnabarLabFossilRoom": 0x760e0, + "Warps_CinnabarPokecenter": 0x76136, + "Shop8": 0x7617d, + "Warps_CinnabarMart": 0x761a9, + "Warps_CopycatsHouse1F": 0x761fb, + "Starter2_N": 0x762b0, + "Starter3_N": 0x762b8, + "Warps_ChampionsRoom": 0x764e3, + "Trainersanity_EVENT_BEAT_LORELEIS_ROOM_TRAINER_0_ITEM": 0x76612, + "Warps_LoreleisRoom": 0x76636, + "Trainersanity_EVENT_BEAT_BRUNOS_ROOM_TRAINER_0_ITEM": 0x7676b, + "Warps_BrunosRoom": 0x7678f, + "Trainersanity_EVENT_BEAT_AGATHAS_ROOM_TRAINER_0_ITEM": 0x768ca, + "Warps_AgathasRoom": 0x768ee, + "Option_Itemfinder": 0x76a41, "Text_Quiz_A": 0x88806, "Text_Quiz_B": 0x8893a, "Text_Quiz_C": 0x88a6e, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 1d68f3148963..ba4bfd471c52 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -3,7 +3,7 @@ from . import logic -def set_rules(multiworld, player): +def set_rules(multiworld, world, player): item_rules = { # Some items do special things when they are passed into the GiveItem function in the game, but @@ -15,54 +15,46 @@ def set_rules(multiworld, player): not in i.name) } - if multiworld.prizesanity[player]: + if world.options.prizesanity: def prize_rule(i): return i.player != player or i.name in item_groups["Unique"] item_rules["Celadon Prize Corner - Item Prize 1"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule - if multiworld.accessibility[player] != "full": - multiworld.get_location("Cerulean Bicycle Shop", player).always_allow = (lambda state, item: - item.name == "Bike Voucher" - and item.player == player) - multiworld.get_location("Fuchsia Warden's House - Safari Zone Warden", player).always_allow = (lambda state, item: - item.name == "Gold Teeth" and - item.player == player) - access_rules = { "Rival's House - Rival's Sister": lambda state: state.has("Oak's Parcel", player), "Oak's Lab - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player), - "Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, player) or logic.can_surf(state, player), - "Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_2[player].value + 5, player), + "Viridian City - Sleepy Guy": lambda state: logic.can_cut(state, world, player) or logic.can_surf(state, world, player), + "Route 2 Gate - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_2.value + 5, player), "Cerulean Bicycle Shop": lambda state: state.has("Bike Voucher", player) or location_item_name(state, "Cerulean Bicycle Shop", player) == ("Bike Voucher", player), "Lavender Mr. Fuji's House - Mr. Fuji": lambda state: state.has("Fuji Saved", player), - "Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_11[player].value + 5, player), - "Celadon City - Stranded Man": lambda state: logic.can_surf(state, player), + "Route 11 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_11.value + 5, player), + "Celadon City - Stranded Man": lambda state: logic.can_surf(state, world, player), "Fuchsia Warden's House - Safari Zone Warden": lambda state: state.has("Gold Teeth", player) or location_item_name(state, "Fuchsia Warden's House - Safari Zone Warden", player) == ("Gold Teeth", player), - "Route 12 - Island Item": lambda state: logic.can_surf(state, player), - "Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, state.multiworld.oaks_aide_rt_15[player].value + 5, player), - "Route 25 - Item": lambda state: logic.can_cut(state, player), - "Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, player), - "Safari Zone Center - Island Item": lambda state: logic.can_surf(state, player), + "Route 12 - Island Item": lambda state: logic.can_surf(state, world, player), + "Route 15 Gate 2F - Oak's Aide": lambda state: logic.oaks_aide(state, world, world.options.oaks_aide_rt_15.value + 5, player), + "Route 25 - Item": lambda state: logic.can_cut(state, world, player), + "Fuchsia Warden's House - Behind Boulder Item": lambda state: logic.can_strength(state, world, player), + "Safari Zone Center - Island Item": lambda state: logic.can_surf(state, world, player), "Saffron Copycat's House 2F - Copycat": lambda state: state.has("Buy Poke Doll", player), "Celadon Game Corner - West Gambler's Gift": lambda state: state.has("Coin Case", player), "Celadon Game Corner - Center Gambler's Gift": lambda state: state.has("Coin Case", player), "Celadon Game Corner - East Gambler's Gift": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), - "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, player), + "Celadon Game Corner - Hidden Item Northwest By Counter": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Southwest Corner": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Rumor Man": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Speculating Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near West Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Wonderful Time Woman": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy": lambda state: state.has( "Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near East Gifting Gambler": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item Near Hooked Guy": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), + "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row": lambda state: state.has("Coin Case", player) and logic.can_get_hidden_items(state, world, player), "Celadon Prize Corner - Item Prize 1": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player), "Celadon Prize Corner - Item Prize 2": lambda state: state.has("Coin Case", player) and state.has("Game Corner", player), @@ -79,9 +71,9 @@ def prize_rule(i): "Cinnabar Lab Fossil Room - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player) and state.has("Cinnabar Island", player), "Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), "Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player), - "Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, player) and state.has("Seafoam Boss Boulders", player), - "Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, player), - "Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, player), + "Seafoam Islands B4F - Legendary Pokemon": lambda state: logic.can_strength(state, world, player) and state.has("Seafoam Boss Boulders", player), + "Vermilion Dock - Legendary Pokemon": lambda state: logic.can_surf(state, world, player), + "Cerulean Cave B1F - Legendary Pokemon": lambda state: logic.can_surf(state, world, player), **{f"Pokemon Tower {floor}F - Wild Pokemon - {slot}": lambda state: state.has("Silph Scope", player) for floor in range(3, 8) for slot in range(1, 11)}, "Pokemon Tower 6F - Restless Soul": lambda state: state.has("Silph Scope", player), # just for level scaling @@ -103,101 +95,101 @@ def prize_rule(i): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), # # Rock Tunnel - "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player), - "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, world, player), + "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, world, player), # Pokédex check "Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player), # Hidden items - "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state, + "Viridian Forest - Hidden Item Northwest by Trainer": lambda state: logic.can_get_hidden_items(state, world, player), - "Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, player), - "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state, + "Viridian Forest - Hidden Item Entrance Tree": lambda state: logic.can_get_hidden_items(state, world, player), + "Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state, + "Route 25 - Hidden Item Fence Outside Bill's House": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, player), - "S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, player), - "S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, player), + "Route 9 - Hidden Item Bush By Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "S.S. Anne Kitchen - Hidden Item Kitchen Trash": lambda state: logic.can_get_hidden_items(state, world, player), + "S.S. Anne B1F Rooms - Hidden Item Under Pillow": lambda state: logic.can_get_hidden_items(state, world, player), "Route 10 - Hidden Item Behind Rock Tunnel Entrance Cuttable Tree": lambda - state: logic.can_get_hidden_items(state, player) and logic.can_cut(state, player), - "Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, player), - "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player), - "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, player), + state: logic.can_get_hidden_items(state, world, player) and logic.can_cut(state, world, player), + "Route 10 - Hidden Item Bush": lambda state: logic.can_get_hidden_items(state, world, player), + "Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player), + "Rocket Hideout B3F - Hidden Item Near East Item": lambda state: logic.can_get_hidden_items(state, world, player), "Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: - logic.can_get_hidden_items(state, player), - "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state, + logic.can_get_hidden_items(state, world, player), + "Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, player), - "Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, player), - "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state, + "Route 13 - Hidden Item Dead End Bush": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 13 - Hidden Item Dead End By Water Corner": lambda state: logic.can_get_hidden_items(state, world, player), + "Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: logic.can_get_hidden_items(state, world, player), - "Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state, + "Safari Zone West - Hidden Item Secret House Statue": lambda state: logic.can_get_hidden_items(state, world, player), - "Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, player), - "Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, player), - "Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, player), - "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, player), - "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, player), - "Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, player), - "Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, player), - "Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player), - "Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, player), + "Silph Co 5F - Hidden Item Pot Plant": lambda state: logic.can_get_hidden_items(state, world, player), + "Silph Co 9F - Hidden Item Nurse Bed": lambda state: logic.can_get_hidden_items(state, world, player), + "Saffron Copycat's House 2F - Hidden Item Desk": lambda state: logic.can_get_hidden_items(state, world, player), + "Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: logic.can_get_hidden_items(state, world, player), + "Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: logic.can_get_hidden_items(state, world, player), + "Power Plant - Hidden Item Central Dead End": lambda state: logic.can_get_hidden_items(state, world, player), + "Power Plant - Hidden Item Before Zapdos": lambda state: logic.can_get_hidden_items(state, world, player), + "Seafoam Islands B2F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player), + "Seafoam Islands B3F - Hidden Item Rock": lambda state: logic.can_get_hidden_items(state, world, player), # if you can reach any exit boulders, that means you can drop into the water tunnel and auto-surf - "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, player), + "Seafoam Islands B4F - Hidden Item Corner Island": lambda state: logic.can_get_hidden_items(state, world, player), "Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda - state: logic.can_get_hidden_items(state, player), - "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, player), - "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state, + state: logic.can_get_hidden_items(state, world, player), + "Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 23 - Hidden Item Rocks Before Victory Road": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state, + "Route 23 - Hidden Item East Bush After Water": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state, - player) and logic.can_surf(state, player), - "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state, + "Route 23 - Hidden Item On Island": lambda state: logic.can_get_hidden_items(state, world, + player) and logic.can_surf(state, world, player), + "Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: logic.can_get_hidden_items(state, world, player), - "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, player), - "Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, player), - "Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player), - "Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, player), - "Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, player), + "Victory Road 2F - Hidden Item Rock In Final Room": lambda state: logic.can_get_hidden_items(state, world, player), + "Viridian City - Hidden Item Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 11 - Hidden Item Isolated Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 12 - Hidden Item Bush Near Gate": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item In Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item Near Northernmost Sign": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item East Center": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item West Center": lambda state: logic.can_get_hidden_items(state, world, player), + "Route 17 - Hidden Item Before Final Bridge": lambda state: logic.can_get_hidden_items(state, world, player), "Underground Path North South - Hidden Item Near Northern Stairs": lambda - state: logic.can_get_hidden_items(state, player), + state: logic.can_get_hidden_items(state, world, player), "Underground Path North South - Hidden Item Near Southern Stairs": lambda - state: logic.can_get_hidden_items(state, player), - "Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, player), - "Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, player), - "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state, + state: logic.can_get_hidden_items(state, world, player), + "Underground Path West East - Hidden Item West": lambda state: logic.can_get_hidden_items(state, world, player), + "Underground Path West East - Hidden Item East": lambda state: logic.can_get_hidden_items(state, world, player), + "Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, player), - "Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, player), - "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state, - player) and logic.can_surf(state, player), - "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state, + "Route 25 - Hidden Item Northeast Of Grass": lambda state: logic.can_get_hidden_items(state, world, player), + "Mt Moon B2F - Hidden Item Lone Rock": lambda state: logic.can_get_hidden_items(state, world, player), + "Vermilion City - Hidden Item In Water Near Fan Club": lambda state: logic.can_get_hidden_items(state, world, + player) and logic.can_surf(state, world, player), + "Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: logic.can_get_hidden_items(state, world, player), - "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, player), + "Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: logic.can_get_hidden_items(state, world, player), # Evolutions "Evolution - Ivysaur": lambda state: state.has("Bulbasaur", player) and logic.evolve_level(state, 16, player), @@ -281,5 +273,4 @@ def prize_rule(i): if loc.name.startswith("Pokedex"): mon = loc.name.split(" - ")[1] add_rule(loc, lambda state, i=mon: (state.has("Pokedex", player) or not - state.multiworld.require_pokedex[player]) and (state.has(i, player) - or state.has(f"Static {i}", player))) + world.options.require_pokedex) and (state.has(i, player) or state.has(f"Static {i}", player))) diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index d8298c85c8fb..139ff6094427 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -1,6 +1,6 @@ -from typing import Dict +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet +from dataclasses import dataclass class StartingGender(Choice): @@ -175,13 +175,21 @@ class NumberOfChildren(Range): default = 3 -class AdditionalNames(OptionSet): +class AdditionalLadyNames(OptionSet): """ Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list of names your children can have. The first value will also be your initial character's name depending on Starting Gender. """ - display_name = "Additional Names" + display_name = "Additional Lady Names" + +class AdditionalSirNames(OptionSet): + """ + Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list + of names your children can have. The first value will also be your initial character's name depending on Starting + Gender. + """ + display_name = "Additional Sir Names" class AllowDefaultNames(DefaultOnToggle): @@ -336,42 +344,44 @@ class AvailableClasses(OptionSet): The upgraded form of your starting class will be available regardless. """ display_name = "Available Classes" - default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + default = frozenset( + {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + ) valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} -rl_options: Dict[str, type(Option)] = { - "starting_gender": StartingGender, - "starting_class": StartingClass, - "available_classes": AvailableClasses, - "new_game_plus": NewGamePlus, - "fairy_chests_per_zone": FairyChestsPerZone, - "chests_per_zone": ChestsPerZone, - "universal_fairy_chests": UniversalFairyChests, - "universal_chests": UniversalChests, - "vendors": Vendors, - "architect": Architect, - "architect_fee": ArchitectFee, - "disable_charon": DisableCharon, - "require_purchasing": RequirePurchasing, - "progressive_blueprints": ProgressiveBlueprints, - "gold_gain_multiplier": GoldGainMultiplier, - "number_of_children": NumberOfChildren, - "free_diary_on_generation": FreeDiaryOnGeneration, - "khidr": ChallengeBossKhidr, - "alexander": ChallengeBossAlexander, - "leon": ChallengeBossLeon, - "herodotus": ChallengeBossHerodotus, - "health_pool": HealthUpPool, - "mana_pool": ManaUpPool, - "attack_pool": AttackUpPool, - "magic_damage_pool": MagicDamageUpPool, - "armor_pool": ArmorUpPool, - "equip_pool": EquipUpPool, - "crit_chance_pool": CritChanceUpPool, - "crit_damage_pool": CritDamageUpPool, - "allow_default_names": AllowDefaultNames, - "additional_lady_names": AdditionalNames, - "additional_sir_names": AdditionalNames, - "death_link": DeathLink, -} +@dataclass +class RLOptions(PerGameCommonOptions): + starting_gender: StartingGender + starting_class: StartingClass + available_classes: AvailableClasses + new_game_plus: NewGamePlus + fairy_chests_per_zone: FairyChestsPerZone + chests_per_zone: ChestsPerZone + universal_fairy_chests: UniversalFairyChests + universal_chests: UniversalChests + vendors: Vendors + architect: Architect + architect_fee: ArchitectFee + disable_charon: DisableCharon + require_purchasing: RequirePurchasing + progressive_blueprints: ProgressiveBlueprints + gold_gain_multiplier: GoldGainMultiplier + number_of_children: NumberOfChildren + free_diary_on_generation: FreeDiaryOnGeneration + khidr: ChallengeBossKhidr + alexander: ChallengeBossAlexander + leon: ChallengeBossLeon + herodotus: ChallengeBossHerodotus + health_pool: HealthUpPool + mana_pool: ManaUpPool + attack_pool: AttackUpPool + magic_damage_pool: MagicDamageUpPool + armor_pool: ArmorUpPool + equip_pool: EquipUpPool + crit_chance_pool: CritChanceUpPool + crit_damage_pool: CritDamageUpPool + allow_default_names: AllowDefaultNames + additional_lady_names: AdditionalLadyNames + additional_sir_names: AdditionalSirNames + death_link: DeathLink diff --git a/worlds/rogue_legacy/Regions.py b/worlds/rogue_legacy/Regions.py index 5d07fccbc4d4..61b0ef73ec78 100644 --- a/worlds/rogue_legacy/Regions.py +++ b/worlds/rogue_legacy/Regions.py @@ -1,15 +1,18 @@ -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING from BaseClasses import MultiWorld, Region, Entrance from .Locations import RLLocation, location_table, get_locations_by_category +if TYPE_CHECKING: + from . import RLWorld + class RLRegionData(NamedTuple): locations: Optional[List[str]] region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_regions(world: "RLWorld"): regions: Dict[str, RLRegionData] = { "Menu": RLRegionData(None, ["Castle Hamson"]), "The Manor": RLRegionData([], []), @@ -56,9 +59,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["The Fountain Room"].locations.append("Fountain Room") # Chests - chests = int(multiworld.chests_per_zone[player]) + chests = int(world.options.chests_per_zone) for i in range(0, chests): - if multiworld.universal_chests[player]: + if world.options.universal_chests: regions["Castle Hamson"].locations.append(f"Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}") @@ -70,9 +73,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}") # Fairy Chests - chests = int(multiworld.fairy_chests_per_zone[player]) + chests = int(world.options.fairy_chests_per_zone) for i in range(0, chests): - if multiworld.universal_fairy_chests[player]: + if world.options.universal_fairy_chests: regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}") @@ -85,14 +88,14 @@ def create_regions(multiworld: MultiWorld, player: int): # Set up the regions correctly. for name, data in regions.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) - - multiworld.get_entrance("Castle Hamson", player).connect(multiworld.get_region("Castle Hamson", player)) - multiworld.get_entrance("The Manor", player).connect(multiworld.get_region("The Manor", player)) - multiworld.get_entrance("Forest Abkhazia", player).connect(multiworld.get_region("Forest Abkhazia", player)) - multiworld.get_entrance("The Maya", player).connect(multiworld.get_region("The Maya", player)) - multiworld.get_entrance("Land of Darkness", player).connect(multiworld.get_region("Land of Darkness", player)) - multiworld.get_entrance("The Fountain Room", player).connect(multiworld.get_region("The Fountain Room", player)) + world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data)) + + world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson")) + world.get_entrance("The Manor").connect(world.get_region("The Manor")) + world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia")) + world.get_entrance("The Maya").connect(world.get_region("The Maya")) + world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness")) + world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room")) def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData): diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py index 2fac8d561399..505bbdd63541 100644 --- a/worlds/rogue_legacy/Rules.py +++ b/worlds/rogue_legacy/Rules.py @@ -1,9 +1,13 @@ -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import RLWorld -def get_upgrade_total(multiworld: MultiWorld, player: int) -> int: - return int(multiworld.health_pool[player]) + int(multiworld.mana_pool[player]) + \ - int(multiworld.attack_pool[player]) + int(multiworld.magic_damage_pool[player]) + +def get_upgrade_total(world: "RLWorld") -> int: + return int(world.options.health_pool) + int(world.options.mana_pool) + \ + int(world.options.attack_pool) + int(world.options.magic_damage_pool) def get_upgrade_count(state: CollectionState, player: int) -> int: @@ -19,8 +23,8 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool return get_upgrade_count(state, player) >= amount -def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool: - return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100))) +def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool: + return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100))) def has_movement_rune(state: CollectionState, player: int) -> bool: @@ -47,15 +51,15 @@ def has_defeated_dungeon(state: CollectionState, player: int) -> bool: return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player) -def set_rules(multiworld: MultiWorld, player: int): +def set_rules(world: "RLWorld", player: int): # If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres. - if multiworld.vendors[player] == "normal": - multiworld.get_location("Forest Abkhazia Boss Reward", player).access_rule = \ + if world.options.vendors == "normal": + world.get_location("Forest Abkhazia Boss Reward").access_rule = \ lambda state: has_vendors(state, player) # Gate each manor location so everything isn't dumped into sphere 1. manor_rules = { - "Defeat Khidr" if multiworld.khidr[player] == "vanilla" else "Defeat Neo Khidr": [ + "Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [ "Manor - Left Wing Window", "Manor - Left Wing Rooftop", "Manor - Right Wing Window", @@ -66,7 +70,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Left Tree 2", "Manor - Right Tree", ], - "Defeat Alexander" if multiworld.alexander[player] == "vanilla" else "Defeat Alexander IV": [ + "Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [ "Manor - Left Big Upper 1", "Manor - Left Big Upper 2", "Manor - Left Big Windows", @@ -78,7 +82,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Right Big Rooftop", "Manor - Right Extension", ], - "Defeat Ponce de Leon" if multiworld.leon[player] == "vanilla" else "Defeat Ponce de Freon": [ + "Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [ "Manor - Right High Base", "Manor - Right High Upper", "Manor - Right High Tower", @@ -90,24 +94,24 @@ def set_rules(multiworld: MultiWorld, player: int): # Set rules for manor locations. for event, locations in manor_rules.items(): for location in locations: - multiworld.get_location(location, player).access_rule = lambda state: state.has(event, player) + world.get_location(location).access_rule = lambda state: state.has(event, player) # Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests. - for fairy_location in [location for location in multiworld.get_locations(player) if "Fairy" in location.name]: + for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]: fairy_location.access_rule = lambda state: has_fairy_progression(state, player) # Region rules. - multiworld.get_entrance("Forest Abkhazia", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 12.5) and has_defeated_castle(state, player) + world.get_entrance("Forest Abkhazia").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player) - multiworld.get_entrance("The Maya", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 25) and has_defeated_forest(state, player) + world.get_entrance("The Maya").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player) - multiworld.get_entrance("Land of Darkness", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 37.5) and has_defeated_tower(state, player) + world.get_entrance("Land of Darkness").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player) - multiworld.get_entrance("The Fountain Room", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 50) and has_defeated_dungeon(state, player) + world.get_entrance("The Fountain Room").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player) # Win condition. - multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) + world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index eb657699540f..290f4a60ac21 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -4,7 +4,7 @@ from worlds.AutoWorld import WebWorld, World from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table from .Locations import RLLocation, location_table -from .Options import rl_options +from .Options import RLOptions from .Presets import rl_options_presets from .Regions import create_regions from .Rules import set_rules @@ -33,35 +33,56 @@ class RLWorld(World): But that's OK, because no one is perfect, and you don't have to be to succeed. """ game = "Rogue Legacy" - option_definitions = rl_options + options_dataclass = RLOptions + options: RLOptions topology_present = True required_client_version = (0, 3, 5) web = RLWeb() - item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {name: data.code for name, data in location_table.items()} - - # TODO: Replace calls to this function with "options-dict", once that PR is completed and merged. - def get_setting(self, name: str): - return getattr(self.multiworld, name)[self.player] + item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None} + location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None} def fill_slot_data(self) -> dict: - return {option_name: self.get_setting(option_name).value for option_name in rl_options} + return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) 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) - if not self.get_setting("allow_default_names"): - if additional_lady_names < int(self.get_setting("number_of_children")): + additional_lady_names = len(self.options.additional_lady_names.value) + additional_sir_names = len(self.options.additional_sir_names.value) + if not self.options.allow_default_names: + if additional_lady_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_lady_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_lady_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}") - if additional_sir_names < int(self.get_setting("number_of_children")): + if additional_sir_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_sir_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_sir_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}") def create_items(self): item_pool: List[RLItem] = [] @@ -71,110 +92,110 @@ def create_items(self): # Architect if name == "Architect": - if self.get_setting("architect") == "disabled": + if self.options.architect == "disabled": continue - if self.get_setting("architect") == "start_unlocked": + if self.options.architect == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("architect") == "early": + if self.options.architect == "early": self.multiworld.local_early_items[self.player]["Architect"] = 1 # Blacksmith and Enchantress if name == "Blacksmith" or name == "Enchantress": - if self.get_setting("vendors") == "start_unlocked": + if self.options.vendors == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("vendors") == "early": + if self.options.vendors == "early": self.multiworld.local_early_items[self.player]["Blacksmith"] = 1 self.multiworld.local_early_items[self.player]["Enchantress"] = 1 # Haggling - if name == "Haggling" and self.get_setting("disable_charon"): + if name == "Haggling" and self.options.disable_charon: continue # Blueprints if data.category == "Blueprints": # No progressive blueprints if progressive_blueprints are disabled. - if name == "Progressive Blueprints" and not self.get_setting("progressive_blueprints"): + if name == "Progressive Blueprints" and not self.options.progressive_blueprints: continue # No distinct blueprints if progressive_blueprints are enabled. - elif name != "Progressive Blueprints" and self.get_setting("progressive_blueprints"): + elif name != "Progressive Blueprints" and self.options.progressive_blueprints: continue # Classes if data.category == "Classes": if name == "Progressive Knights": - if "Knight" not in self.get_setting("available_classes"): + if "Knight" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knight": + if self.options.starting_class == "knight": quantity = 1 if name == "Progressive Mages": - if "Mage" not in self.get_setting("available_classes"): + if "Mage" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "mage": + if self.options.starting_class == "mage": quantity = 1 if name == "Progressive Barbarians": - if "Barbarian" not in self.get_setting("available_classes"): + if "Barbarian" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "barbarian": + if self.options.starting_class == "barbarian": quantity = 1 if name == "Progressive Knaves": - if "Knave" not in self.get_setting("available_classes"): + if "Knave" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knave": + if self.options.starting_class == "knave": quantity = 1 if name == "Progressive Miners": - if "Miner" not in self.get_setting("available_classes"): + if "Miner" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "miner": + if self.options.starting_class == "miner": quantity = 1 if name == "Progressive Shinobis": - if "Shinobi" not in self.get_setting("available_classes"): + if "Shinobi" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "shinobi": + if self.options.starting_class == "shinobi": quantity = 1 if name == "Progressive Liches": - if "Lich" not in self.get_setting("available_classes"): + if "Lich" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "lich": + if self.options.starting_class == "lich": quantity = 1 if name == "Progressive Spellthieves": - if "Spellthief" not in self.get_setting("available_classes"): + if "Spellthief" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "spellthief": + if self.options.starting_class == "spellthief": quantity = 1 if name == "Dragons": - if "Dragon" not in self.get_setting("available_classes"): + if "Dragon" not in self.options.available_classes: continue if name == "Traitors": - if "Traitor" not in self.get_setting("available_classes"): + if "Traitor" not in self.options.available_classes: continue # Skills if name == "Health Up": - quantity = self.get_setting("health_pool") + quantity = self.options.health_pool.value elif name == "Mana Up": - quantity = self.get_setting("mana_pool") + quantity = self.options.mana_pool.value elif name == "Attack Up": - quantity = self.get_setting("attack_pool") + quantity = self.options.attack_pool.value elif name == "Magic Damage Up": - quantity = self.get_setting("magic_damage_pool") + quantity = self.options.magic_damage_pool.value elif name == "Armor Up": - quantity = self.get_setting("armor_pool") + quantity = self.options.armor_pool.value elif name == "Equip Up": - quantity = self.get_setting("equip_pool") + quantity = self.options.equip_pool.value elif name == "Crit Chance Up": - quantity = self.get_setting("crit_chance_pool") + quantity = self.options.crit_chance_pool.value elif name == "Crit Damage Up": - quantity = self.get_setting("crit_damage_pool") + quantity = self.options.crit_damage_pool.value # Ignore filler, it will be added in a later stage. if data.category == "Filler": @@ -191,7 +212,7 @@ def create_items(self): def get_filler_item_name(self) -> str: fillers = get_items_by_category("Filler") weights = [data.weight for data in fillers.values()] - return self.multiworld.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] + return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] def create_item(self, name: str) -> RLItem: data = item_table[name] @@ -202,10 +223,10 @@ def create_event(self, name: str) -> RLItem: return RLItem(name, data.classification, data.code, self.player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self, self.player) def create_regions(self): - create_regions(self.multiworld, self.player) + create_regions(self) self._place_events() def _place_events(self): @@ -214,7 +235,7 @@ def _place_events(self): self.create_event("Defeat The Fountain")) # Khidr / Neo Khidr - if self.get_setting("khidr") == "vanilla": + if self.options.khidr == "vanilla": self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item( self.create_event("Defeat Khidr")) else: @@ -222,7 +243,7 @@ def _place_events(self): self.create_event("Defeat Neo Khidr")) # Alexander / Alexander IV - if self.get_setting("alexander") == "vanilla": + if self.options.alexander == "vanilla": self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item( self.create_event("Defeat Alexander")) else: @@ -230,7 +251,7 @@ def _place_events(self): self.create_event("Defeat Alexander IV")) # Ponce de Leon / Ponce de Freon - if self.get_setting("leon") == "vanilla": + if self.options.leon == "vanilla": self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item( self.create_event("Defeat Ponce de Leon")) else: @@ -238,7 +259,7 @@ def _place_events(self): self.create_event("Defeat Ponce de Freon")) # Herodotus / Astrodotus - if self.get_setting("herodotus") == "vanilla": + if self.options.herodotus == "vanilla": self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item( self.create_event("Defeat Herodotus")) else: diff --git a/worlds/rogue_legacy/test/__init__.py b/worlds/rogue_legacy/test/__init__.py index 2639e618c678..3346476ba644 100644 --- a/worlds/rogue_legacy/test/__init__.py +++ b/worlds/rogue_legacy/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class RLTestBase(WorldTestBase): diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index bb325ba1da45..813cf2884517 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -97,12 +97,12 @@ class ConfigurableOptionInfo(typing.NamedTuple): class ColouredMessage: - def __init__(self, text: str = '') -> None: + def __init__(self, text: str = '', *, keep_markup: bool = False) -> None: self.parts: typing.List[dict] = [] if text: - self(text) - def __call__(self, text: str) -> 'ColouredMessage': - add_json_text(self.parts, text) + self(text, keep_markup=keep_markup) + def __call__(self, text: str, *, keep_markup: bool = False) -> 'ColouredMessage': + add_json_text(self.parts, text, keep_markup=keep_markup) return self def coloured(self, text: str, colour: str) -> 'ColouredMessage': add_json_text(self.parts, text, type="color", color=colour) @@ -128,7 +128,7 @@ def formatted_print(self, text: str) -> None: # Note(mm): Bold/underline can help readability, but unfortunately the CommonClient does not filter bold tags from command-line output. # Regardless, using `on_print_json` to get formatted text in the GUI and output in the command-line and in the logs, # without having to branch code from CommonClient - self.ctx.on_print_json({"data": [{"text": text}]}) + self.ctx.on_print_json({"data": [{"text": text, "keep_markup": True}]}) def _cmd_difficulty(self, difficulty: str = "") -> bool: """Overrides the current difficulty set for the world. Takes the argument casual, normal, hard, or brutal""" @@ -257,7 +257,7 @@ def print_faction_title(): print_faction_title() has_printed_faction_title = True (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, self.ctx.slot) + (" from ").location(item.location, item.player) (" by ").player(item.player) ).send(self.ctx) @@ -278,7 +278,7 @@ def print_faction_title(): for item in received_items_of_this_type: filter_match_count += len(received_items_of_this_type) (ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags) - (" from ").location(item.location, self.ctx.slot) + (" from ").location(item.location, item.player) (" by ").player(item.player) ).send(self.ctx) diff --git a/worlds/sc2/ClientGui.py b/worlds/sc2/ClientGui.py index 22e444efe7c9..51c55b437d92 100644 --- a/worlds/sc2/ClientGui.py +++ b/worlds/sc2/ClientGui.py @@ -1,7 +1,8 @@ from typing import * import asyncio -from kvui import GameManager, HoverBehavior, ServerToolTip +from NetUtils import JSONMessagePart +from kvui import GameManager, HoverBehavior, ServerToolTip, KivyJSONtoTextParser from kivy.app import App from kivy.clock import Clock from kivy.uix.tabbedpanel import TabbedPanelItem @@ -69,6 +70,18 @@ class MissionLayout(GridLayout): class MissionCategory(GridLayout): pass + +class SC2JSONtoKivyParser(KivyJSONtoTextParser): + def _handle_text(self, node: JSONMessagePart): + if node.get("keep_markup", False): + for ref in node.get("refs", []): + node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" + self.ref_count += 1 + return super(KivyJSONtoTextParser, self)._handle_text(node) + else: + return super()._handle_text(node) + + class SC2Manager(GameManager): logging_pairs = [ ("Client", "Archipelago"), @@ -87,6 +100,7 @@ class SC2Manager(GameManager): def __init__(self, ctx) -> None: super().__init__(ctx) + self.json_to_kivy_parser = SC2JSONtoKivyParser(ctx) def clear_tooltip(self) -> None: if self.ctx.current_tooltip: @@ -97,13 +111,10 @@ def clear_tooltip(self) -> None: def build(self): container = super().build() - panel = TabbedPanelItem(text="Starcraft 2 Launcher") - panel.content = CampaignScroll() + panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll()) self.campaign_panel = MultiCampaignLayout() panel.content.add_widget(self.campaign_panel) - self.tabs.add_widget(panel) - Clock.schedule_interval(self.build_mission_table, 0.5) return container diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py index 8277d0e7e13d..ee1f34d75be9 100644 --- a/worlds/sc2/Items.py +++ b/worlds/sc2/Items.py @@ -1274,16 +1274,16 @@ def get_full_item_list(): description="Defensive structure. Slows the attack and movement speeds of all nearby Zerg units."), ItemNames.STRUCTURE_ARMOR: ItemData(620 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9, SC2Race.TERRAN, - description="Increases armor of all Terran structures by 2."), + description="Increases armor of all Terran structures by 2.", origin={"ext"}), ItemNames.HI_SEC_AUTO_TRACKING: ItemData(621 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, SC2Race.TERRAN, - description="Increases attack range of all Terran structures by 1."), + description="Increases attack range of all Terran structures by 1.", origin={"ext"}), ItemNames.ADVANCED_OPTICS: ItemData(622 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, SC2Race.TERRAN, - description="Increases attack range of all Terran mechanical units by 1."), + description="Increases attack range of all Terran mechanical units by 1.", origin={"ext"}), ItemNames.ROGUE_FORCES: ItemData(623 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, SC2Race.TERRAN, - description="Mercenary calldowns are no longer limited by charges."), + description="Mercenary calldowns are no longer limited by charges.", origin={"ext"}), ItemNames.ZEALOT: ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Unit", 0, SC2Race.PROTOSS, @@ -2369,7 +2369,8 @@ def get_basic_units(world: World, race: SC2Race) -> typing.Set[str]: ItemNames.BATTLECRUISER_PROGRESSIVE_MISSILE_PODS, ItemNames.THOR_PROGRESSIVE_IMMORTALITY_PROTOCOL, ItemNames.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM, - ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL + ItemNames.DIAMONDBACK_PROGRESSIVE_TRI_LITHIUM_POWER_CELL, + ItemNames.PROGRESSIVE_ORBITAL_COMMAND } kerrigan_actives: typing.List[typing.Set[str]] = [ diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md index 06464e3cd2fd..813fdb5f4a2b 100644 --- a/worlds/sc2/docs/en_Starcraft 2.md +++ b/worlds/sc2/docs/en_Starcraft 2.md @@ -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. diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md index 4fcc8e689baa..092835c8e323 100644 --- a/worlds/sc2/docs/fr_Starcraft 2.md +++ b/worlds/sc2/docs/fr_Starcraft 2.md @@ -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é. diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 991ed57e8741..5b378873f4a3 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -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. diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md index bb6c35bce1c7..d9b754572a66 100644 --- a/worlds/sc2/docs/setup_fr.md +++ b/worlds/sc2/docs/setup_fr.md @@ -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. diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py index 2f33eb18e5d1..72791bef3e7b 100644 --- a/worlds/shivers/Options.py +++ b/worlds/shivers/Options.py @@ -92,6 +92,21 @@ class FullPots(Choice): option_mixed = 2 +class PuzzleCollectBehavior(Choice): + """ + Defines what happens to puzzles on collect. + - Solve None: No puzzles will be solved when collected. + - Prevent Out Of Logic Access: All puzzles, except Red Door and Skull Door, will be solved when collected. + This prevents out of logic access to Gods Room and Slide. + - Solve All: All puzzles will be solved when collected. (original behavior) + """ + display_name = "Puzzle Collect Behavior" + option_solve_none = 0 + option_prevent_out_of_logic_access = 1 + option_solve_all = 2 + default = 1 + + @dataclass class ShiversOptions(PerGameCommonOptions): ixupi_captures_needed: IxupiCapturesNeeded @@ -104,3 +119,4 @@ class ShiversOptions(PerGameCommonOptions): early_lightning: EarlyLightning location_pot_pieces: LocationPotPieces full_pots: FullPots + puzzle_collect_behavior: PuzzleCollectBehavior diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index a2d7bc14644e..3ca87ae164f2 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -219,7 +219,8 @@ def fill_slot_data(self) -> dict: "ElevatorsStaySolved": self.options.elevators_stay_solved.value, "EarlyBeth": self.options.early_beth.value, "EarlyLightning": self.options.early_lightning.value, - "FrontDoorUsable": self.options.front_door_usable.value + "FrontDoorUsable": self.options.front_door_usable.value, + "PuzzleCollectBehavior": self.options.puzzle_collect_behavior.value, } diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 223179529cf4..3dad16ad3afd 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -1,6 +1,7 @@ import typing -from Options import Choice, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle +from Options import Choice, PerGameCommonOptions, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle from .variaRandomizer.utils.objectives import _goals +from dataclasses import dataclass class StartItemsRemovesFromPool(Toggle): """Remove items in starting inventory from pool.""" @@ -372,62 +373,62 @@ class RelaxedRoundRobinCF(Toggle): """ display_name = "Relaxed round robin Crystal Flash" -sm_options: typing.Dict[str, type(Option)] = { - "start_inventory_removes_from_pool": StartItemsRemovesFromPool, - "preset": Preset, - "start_location": StartLocation, - "remote_items": RemoteItems, - "death_link": DeathLink, - #"majors_split": "Full", - #"scav_num_locs": "10", - #"scav_randomized": "off", - #"scav_escape": "off", - "max_difficulty": MaxDifficulty, - #"progression_speed": "medium", - #"progression_difficulty": "normal", - "morph_placement": MorphPlacement, - #"suits_restriction": SuitsRestriction, - "hide_items": HideItems, - "strict_minors": StrictMinors, - "missile_qty": MissileQty, - "super_qty": SuperQty, - "power_bomb_qty": PowerBombQty, - "minor_qty": MinorQty, - "energy_qty": EnergyQty, - "area_randomization": AreaRandomization, - "area_layout": AreaLayout, - "doors_colors_rando": DoorsColorsRando, - "allow_grey_doors": AllowGreyDoors, - "boss_randomization": BossRandomization, - #"minimizer": "off", - #"minimizer_qty": "45", - #"minimizer_tourian": "off", - "escape_rando": EscapeRando, - "remove_escape_enemies": RemoveEscapeEnemies, - "fun_combat": FunCombat, - "fun_movement": FunMovement, - "fun_suits": FunSuits, - "layout_patches": LayoutPatches, - "varia_tweaks": VariaTweaks, - "nerfed_charge": NerfedCharge, - "gravity_behaviour": GravityBehaviour, - #"item_sounds": "on", - "elevators_speed": ElevatorsSpeed, - "fast_doors": DoorsSpeed, - "spin_jump_restart": SpinJumpRestart, - "rando_speed": SpeedKeep, - "infinite_space_jump": InfiniteSpaceJump, - "refill_before_save": RefillBeforeSave, - "hud": Hud, - "animals": Animals, - "no_music": NoMusic, - "random_music": RandomMusic, - "custom_preset": CustomPreset, - "varia_custom_preset": VariaCustomPreset, - "tourian": Tourian, - "custom_objective": CustomObjective, - "custom_objective_list": CustomObjectiveList, - "custom_objective_count": CustomObjectiveCount, - "objective": Objective, - "relaxed_round_robin_cf": RelaxedRoundRobinCF, - } +@dataclass +class SMOptions(PerGameCommonOptions): + start_inventory_removes_from_pool: StartItemsRemovesFromPool + preset: Preset + start_location: StartLocation + remote_items: RemoteItems + death_link: DeathLink + #majors_split: "Full" + #scav_num_locs: "10" + #scav_randomized: "off" + #scav_escape: "off" + max_difficulty: MaxDifficulty + #progression_speed": "medium" + #progression_difficulty": "normal" + morph_placement: MorphPlacement + #suits_restriction": SuitsRestriction + hide_items: HideItems + strict_minors: StrictMinors + missile_qty: MissileQty + super_qty: SuperQty + power_bomb_qty: PowerBombQty + minor_qty: MinorQty + energy_qty: EnergyQty + area_randomization: AreaRandomization + area_layout: AreaLayout + doors_colors_rando: DoorsColorsRando + allow_grey_doors: AllowGreyDoors + boss_randomization: BossRandomization + #minimizer: "off" + #minimizer_qty: "45" + #minimizer_tourian: "off" + escape_rando: EscapeRando + remove_escape_enemies: RemoveEscapeEnemies + fun_combat: FunCombat + fun_movement: FunMovement + fun_suits: FunSuits + layout_patches: LayoutPatches + varia_tweaks: VariaTweaks + nerfed_charge: NerfedCharge + gravity_behaviour: GravityBehaviour + #item_sounds: "on" + elevators_speed: ElevatorsSpeed + fast_doors: DoorsSpeed + spin_jump_restart: SpinJumpRestart + rando_speed: SpeedKeep + infinite_space_jump: InfiniteSpaceJump + refill_before_save: RefillBeforeSave + hud: Hud + animals: Animals + no_music: NoMusic + random_music: RandomMusic + custom_preset: CustomPreset + varia_custom_preset: VariaCustomPreset + tourian: Tourian + custom_objective: CustomObjective + custom_objective_list: CustomObjectiveList + custom_objective_count: CustomObjectiveCount + objective: Objective + relaxed_round_robin_cf: RelaxedRoundRobinCF diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 826b1447793d..bf9d6d087edd 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -15,7 +15,7 @@ logger = logging.getLogger("Super Metroid") -from .Options import sm_options +from .Options import SMOptions from .Client import SMSNIClient from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols import Utils @@ -96,10 +96,11 @@ class SMWorld(World): a wide range of options to randomize Item locations, required skills and even the connections between the main Areas! """ - game: str = "Super Metroid" topology_present = True - option_definitions = sm_options + options_dataclass = SMOptions + options: SMOptions + settings: typing.ClassVar[SMSettings] item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None} @@ -129,27 +130,27 @@ def generate_early(self): Logic.factory('vanilla') dummy_rom_file = Utils.user_path(SMSettings.RomFile.copy_to) # actual rom set in generate_output - self.variaRando = VariaRandomizer(self.multiworld, dummy_rom_file, self.player) + self.variaRando = VariaRandomizer(self.options, dummy_rom_file, self.player) self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty) # keeps Nothing items local so no player will ever pickup Nothing # doing so reduces contribution of this world to the Multiworld the more Nothing there is though - self.multiworld.local_items[self.player].value.add('Nothing') - self.multiworld.local_items[self.player].value.add('No Energy') + self.options.local_items.value.add('Nothing') + self.options.local_items.value.add('No Energy') if (self.variaRando.args.morphPlacement == "early"): self.multiworld.local_early_items[self.player]['Morph Ball'] = 1 - self.remote_items = self.multiworld.remote_items[self.player] + self.remote_items = self.options.remote_items if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): - self.multiworld.accessibility[self.player].value = Accessibility.option_minimal + self.options.accessibility.value = Accessibility.option_minimal logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings") def create_items(self): itemPool = self.variaRando.container.itemPool self.startItems = [variaItem for item in self.multiworld.precollected_items[self.player] for variaItem in ItemManager.Items.values() if variaItem.Name == item.name] - if self.multiworld.start_inventory_removes_from_pool[self.player]: + if self.options.start_inventory_removes_from_pool: for item in self.startItems: if (item in itemPool): itemPool.remove(item) @@ -317,10 +318,10 @@ def create_item(self, name: str) -> Item: player=self.player) def get_filler_item_name(self) -> str: - if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: - power_bombs = self.multiworld.power_bomb_qty[self.player].value - missiles = self.multiworld.missile_qty[self.player].value - super_missiles = self.multiworld.super_qty[self.player].value + if self.multiworld.random.randint(0, 100) < self.options.minor_qty.value: + power_bombs = self.options.power_bomb_qty.value + missiles = self.options.missile_qty.value + super_missiles = self.options.super_qty.value roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) if roll <= power_bombs: return "Power Bomb" @@ -633,7 +634,7 @@ def APPostPatchRom(self, romPatcher): deathLink: List[ByteEdit] = [{ "sym": symbols["config_deathlink"], "offset": 0, - "values": [self.multiworld.death_link[self.player].value] + "values": [self.options.death_link.value] }] remoteItem: List[ByteEdit] = [{ "sym": symbols["config_remote_items"], @@ -859,10 +860,7 @@ def modify_multidata(self, multidata: dict): def fill_slot_data(self): slot_data = {} if not self.multiworld.is_race: - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = option.value - + slot_data = self.options.as_dict(*self.options_dataclass.type_hints) slot_data["Preset"] = { "Knows": {}, "Settings": {"hardRooms": Settings.SettingsDict[self.player].hardRooms, "bossesDifficulty": Settings.SettingsDict[self.player].bossesDifficulty, @@ -887,14 +885,14 @@ def fill_slot_data(self): return slot_data def write_spoiler(self, spoiler_handle: TextIO): - if self.multiworld.area_randomization[self.player].value != 0: + if self.options.area_randomization.value != 0: spoiler_handle.write('\n\nArea Transitions:\n\n') spoiler_handle.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(self.player)}: ' if self.multiworld.players > 1 else '', src.Name, '<=>', dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if not src.Boss])) - if self.multiworld.boss_randomization[self.player].value != 0: + if self.options.boss_randomization.value != 0: spoiler_handle.write('\n\nBoss Transitions:\n\n') spoiler_handle.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(self.player)}: ' if self.multiworld.players > 1 else '', src.Name, diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index dab078598ec2..8a7a2ea0e2a5 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -250,13 +250,13 @@ class VariaRandomizer: parser.add_argument('--tourianList', help="list to choose from when random", dest='tourianList', nargs='?', default=None) - def __init__(self, world, rom, player): + def __init__(self, options, rom, player): # parse args self.args = copy.deepcopy(VariaRandomizer.parser.parse_args(["--logic", "varia"])) #dummy custom args to skip parsing _sys.argv while still get default values self.player = player args = self.args args.rom = rom - # args.startLocation = to_pascal_case_with_space(world.startLocation[player].current_key) + # args.startLocation = to_pascal_case_with_space(options.startLocation.current_key) if args.output is None and args.rom is None: raise Exception("Need --output or --rom parameter") @@ -288,7 +288,7 @@ def forceArg(arg, value, msg, altValue=None, webArg=None, webValue=None): # print(msg) # optErrMsgs.append(msg) - preset = loadRandoPreset(world, self.player, args) + preset = loadRandoPreset(options, args) # use the skill preset from the rando preset if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None: args.paramsFileName = "/".join((appDir, getPresetDir(preset), preset+".json")) @@ -302,12 +302,12 @@ def forceArg(arg, value, msg, altValue=None, webArg=None, webValue=None): preset = args.preset else: if preset == 'custom': - PresetLoader.factory(world.custom_preset[player].value).load(self.player) + PresetLoader.factory(options.custom_preset.value).load(self.player) elif preset == 'varia_custom': - if len(world.varia_custom_preset[player].value) == 0: + if len(options.varia_custom_preset.value) == 0: raise Exception("varia_custom was chosen but varia_custom_preset is missing.") url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService' - preset_name = next(iter(world.varia_custom_preset[player].value)) + preset_name = next(iter(options.varia_custom_preset.value)) payload = '{{"preset": "{}"}}'.format(preset_name) headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'} response = requests.post(url, data=payload, headers=headers) @@ -463,7 +463,7 @@ def forceArg(arg, value, msg, altValue=None, webArg=None, webValue=None): args.startLocation = random.choice(possibleStartAPs) elif args.startLocation not in possibleStartAPs: args.startLocation = 'Landing Site' - world.start_location[player] = StartLocation(StartLocation.default) + options.start_location = StartLocation(StartLocation.default) #optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation])) #optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs)) #dumpErrorMsgs(args.output, optErrMsgs) diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index 01029f2f6030..f7d699b66549 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -358,35 +358,35 @@ def convertParam(randoParams, param, inverse=False): return "random" raise Exception("invalid value for parameter {}".format(param)) -def loadRandoPreset(world, player, args): +def loadRandoPreset(options, args): defaultMultiValues = getDefaultMultiValues() diffs = ["easy", "medium", "hard", "harder", "hardcore", "mania", "infinity"] presetValues = getPresetValues() - args.animals = world.animals[player].value - args.noVariaTweaks = not world.varia_tweaks[player].value - args.maxDifficulty = diffs[world.max_difficulty[player].value] - #args.suitsRestriction = world.suits_restriction[player].value - args.hideItems = world.hide_items[player].value - args.strictMinors = world.strict_minors[player].value - args.noLayout = not world.layout_patches[player].value - args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value] - args.nerfedCharge = world.nerfed_charge[player].value - args.area = world.area_randomization[player].current_key + args.animals = options.animals.value + args.noVariaTweaks = not options.varia_tweaks.value + args.maxDifficulty = diffs[options.max_difficulty.value] + #args.suitsRestriction = options.suits_restriction.value + args.hideItems = options.hide_items.value + args.strictMinors = options.strict_minors.value + args.noLayout = not options.layout_patches.value + args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][options.gravity_behaviour.value] + args.nerfedCharge = options.nerfed_charge.value + args.area = options.area_randomization.current_key if (args.area == "true"): args.area = "full" if args.area != "off": - args.areaLayoutBase = not world.area_layout[player].value - args.escapeRando = world.escape_rando[player].value - args.noRemoveEscapeEnemies = not world.remove_escape_enemies[player].value - args.doorsColorsRando = world.doors_colors_rando[player].value - args.allowGreyDoors = world.allow_grey_doors[player].value - args.bosses = world.boss_randomization[player].value - if world.fun_combat[player].value: + args.areaLayoutBase = not options.area_layout.value + args.escapeRando = options.escape_rando.value + args.noRemoveEscapeEnemies = not options.remove_escape_enemies.value + args.doorsColorsRando = options.doors_colors_rando.value + args.allowGreyDoors = options.allow_grey_doors.value + args.bosses = options.boss_randomization.value + if options.fun_combat.value: args.superFun.append("Combat") - if world.fun_movement[player].value: + if options.fun_movement.value: args.superFun.append("Movement") - if world.fun_suits[player].value: + if options.fun_suits.value: args.superFun.append("Suits") ipsPatches = { "spin_jump_restart":"spinjumprestart", @@ -396,36 +396,36 @@ def loadRandoPreset(world, player, args): "refill_before_save":"refill_before_save", "relaxed_round_robin_cf":"relaxed_round_robin_cf"} for settingName, patchName in ipsPatches.items(): - if hasattr(world, settingName) and getattr(world, settingName)[player].value: + if hasattr(options, settingName) and getattr(options, settingName).value: args.patches.append(patchName + '.ips') patches = {"no_music":"No_Music", "infinite_space_jump":"Infinite_Space_Jump"} for settingName, patchName in patches.items(): - if hasattr(world, settingName) and getattr(world, settingName)[player].value: + if hasattr(options, settingName) and getattr(options, settingName).value: args.patches.append(patchName) - args.hud = world.hud[player].value - args.morphPlacement = defaultMultiValues["morphPlacement"][world.morph_placement[player].value] + args.hud = options.hud.value + args.morphPlacement = defaultMultiValues["morphPlacement"][options.morph_placement.value] #args.majorsSplit #args.scavNumLocs #args.scavRandomized - args.startLocation = defaultMultiValues["startLocation"][world.start_location[player].value] + args.startLocation = defaultMultiValues["startLocation"][options.start_location.value] #args.progressionDifficulty #args.progressionSpeed - args.missileQty = world.missile_qty[player].value / float(10) - args.superQty = world.super_qty[player].value / float(10) - args.powerBombQty = world.power_bomb_qty[player].value / float(10) - args.minorQty = world.minor_qty[player].value - args.energyQty = defaultMultiValues["energyQty"][world.energy_qty[player].value] - args.objectiveRandom = world.custom_objective[player].value - args.objectiveList = list(world.custom_objective_list[player].value) - args.nbObjective = world.custom_objective_count[player].value - args.objective = list(world.objective[player].value) - args.tourian = defaultMultiValues["tourian"][world.tourian[player].value] + args.missileQty = options.missile_qty.value / float(10) + args.superQty = options.super_qty.value / float(10) + args.powerBombQty = options.power_bomb_qty.value / float(10) + args.minorQty = options.minor_qty.value + args.energyQty = defaultMultiValues["energyQty"][options.energy_qty.value] + args.objectiveRandom = options.custom_objective.value + args.objectiveList = list(options.custom_objective_list.value) + args.nbObjective = options.custom_objective_count.value + args.objective = list(options.objective.value) + args.tourian = defaultMultiValues["tourian"][options.tourian.value] #args.minimizerN #args.minimizerTourian - return presetValues[world.preset[player].value] + return presetValues[options.preset.value] def getRandomizerDefaultParameters(): defaultParams = {} diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 6fc2d74b96dc..52126bcf9ff7 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -246,10 +246,10 @@ def create_regions(world: MultiWorld, options: SM64Options, player: int): regBitS.subregions = [bits_top] -def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): +def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None) -> Entrance: sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) - sourceRegion.connect(targetRegion, rule=rule) + return sourceRegion.connect(targetRegion, rule=rule) def create_region(name: str, player: int, world: MultiWorld) -> SM64Region: diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 9add8d9b2932..1535f9ca1fde 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -92,9 +92,12 @@ def set_rules(world, options: SM64Options, player: int, area_connections: dict, connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"]) connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"], rf.build_rule("GP")) - connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], - lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and - state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + entrance = connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], + lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and + state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) + # Access to "DDD: Board Bowser's Sub" does not require access to other locations or regions, so the only region that + # needs to be registered is its parent region. + world.register_indirect_condition(world.get_location("DDD: Board Bowser's Sub", player).parent_region, entrance) connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2)) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 5983057f7d7a..7456bcb70b62 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -77,9 +77,6 @@ Should your name or password have spaces, enclose it in quotes: `"YourPassword"` Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that. Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text. -**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient. -Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file. - ### Playing offline To play offline, first generate a seed on the game's options page. @@ -129,18 +126,6 @@ To use this batch file, double-click it. A window will open. Type the five-digi Once you provide those two bits of information, the game will open. - If the game only says `Connecting`, try again. Double-check the port number and slot name; even a single typo will cause your connection to fail. -### Addendum - Deleting old saves - -Loading an old Mario save alongside a new seed is a bad idea, as it can cause locked doors and castle secret stars to already be unlocked / obtained. You should avoid opening a save that says "Stars x 0" as opposed to one that simply says "New". - -You can manually delete these old saves in-game before starting a new game, but that can be tedious. With a small edit to the batch files, you can delete these old saves automatically. Just add the line `del %AppData%\sm64ex\*.bin` to the batch file, above the `start` command. For example, here is `offline.bat` with the additional line: - -`del %AppData%\sm64ex\*.bin` - -`start sm64.us.f3dex2e.exe --sm64ap_file %1` - -This extra line deletes any previous save data before opening the game. Don't worry about lost stars or checks - the AP server (or in the case of offline, the `.save` file) keeps track of your star count, unlocked keys/caps/cannons, and which locations have already been checked, so you won't have to redo them. At worst you'll have to rewatch the door unlocking animations, and catch the rabbit Mips twice for his first star again if you haven't yet collected the second one. - ## Installation Troubleshooting Start the game from the command line to view helpful messages regarding SM64EX. @@ -166,8 +151,9 @@ The Japanese Version should have no problem displaying these. ### Toad does not have an item for me. -This happens when you load an existing file that had already received an item from that toad. +This happens on older builds when you load an existing file that had already received an item from that toad. To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress. +Alternatively, updating your build will prevent this issue in the future. ### What happens if I lose connection? diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index 8c5efc431f5c..7df01f8710e1 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, ItemsAccessibility +from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility +from dataclasses import dataclass class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -126,20 +127,19 @@ class EnergyBeep(DefaultOnToggle): """Toggles the low health energy beep in Super Metroid.""" display_name = "Energy Beep" - -smz3_options: typing.Dict[str, type(Option)] = { - "accessibility": ItemsAccessibility, - "sm_logic": SMLogic, - "sword_location": SwordLocation, - "morph_location": MorphLocation, - "goal": Goal, - "key_shuffle": KeyShuffle, - "open_tower": OpenTower, - "ganon_vulnerable": GanonVulnerable, - "open_tourian": OpenTourian, - "spin_jumps_animation": SpinJumpsAnimation, - "heart_beep_speed": HeartBeepSpeed, - "heart_color": HeartColor, - "quick_swap": QuickSwap, - "energy_beep": EnergyBeep - } +@dataclass +class SMZ3Options(PerGameCommonOptions): + accessibility: ItemsAccessibility + sm_logic: SMLogic + sword_location: SwordLocation + morph_location: MorphLocation + goal: Goal + key_shuffle: KeyShuffle + open_tower: OpenTower + ganon_vulnerable: GanonVulnerable + open_tourian: OpenTourian + spin_jumps_animation: SpinJumpsAnimation + heart_beep_speed: HeartBeepSpeed + heart_color: HeartColor + quick_swap: QuickSwap + energy_beep: EnergyBeep diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 690e5172a25c..5e6a6ac60965 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -22,8 +22,8 @@ from .Client import SMZ3SNIClient from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch -from .Options import smz3_options -from Options import Accessibility +from .Options import SMZ3Options +from Options import Accessibility, ItemsAccessibility world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") @@ -68,7 +68,9 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - option_definitions = smz3_options + options_dataclass = SMZ3Options + options: SMZ3Options + item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id @@ -189,14 +191,14 @@ def generate_early(self): self.config = Config() self.config.GameMode = GameMode.Multiworld self.config.Z3Logic = Z3Logic.Normal - self.config.SMLogic = SMLogic(self.multiworld.sm_logic[self.player].value) - self.config.SwordLocation = SwordLocation(self.multiworld.sword_location[self.player].value) - self.config.MorphLocation = MorphLocation(self.multiworld.morph_location[self.player].value) - self.config.Goal = Goal(self.multiworld.goal[self.player].value) - self.config.KeyShuffle = KeyShuffle(self.multiworld.key_shuffle[self.player].value) - self.config.OpenTower = OpenTower(self.multiworld.open_tower[self.player].value) - self.config.GanonVulnerable = GanonVulnerable(self.multiworld.ganon_vulnerable[self.player].value) - self.config.OpenTourian = OpenTourian(self.multiworld.open_tourian[self.player].value) + self.config.SMLogic = SMLogic(self.options.sm_logic.value) + self.config.SwordLocation = SwordLocation(self.options.sword_location.value) + self.config.MorphLocation = MorphLocation(self.options.morph_location.value) + self.config.Goal = Goal(self.options.goal.value) + self.config.KeyShuffle = KeyShuffle(self.options.key_shuffle.value) + self.config.OpenTower = OpenTower(self.options.open_tower.value) + self.config.GanonVulnerable = GanonVulnerable(self.options.ganon_vulnerable.value) + self.config.OpenTourian = OpenTourian(self.options.open_tourian.value) self.local_random = random.Random(self.multiworld.random.randint(0, 1000)) self.smz3World = TotalSMZ3World(self.config, self.multiworld.get_player_name(self.player), self.player, self.multiworld.seed_name) @@ -222,7 +224,7 @@ def create_items(self): else: progressionItems = self.progression # Dungeons items here are not in the itempool and will be prefilled locally so they must stay local - self.multiworld.non_local_items[self.player].value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) + self.options.non_local_items.value -= frozenset(item_name for item_name in self.item_names if TotalSMZ3Item.Item.IsNameDungeonItem(item_name)) for item in self.keyCardsItems: self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item)) @@ -244,7 +246,7 @@ def set_rules(self): set_rule(entrance, lambda state, region=region: region.CanEnter(state.smz3state[self.player])) for loc in region.Locations: l = self.locations[loc.Name] - if self.multiworld.accessibility[self.player] != 'full': + if self.options.accessibility.value != ItemsAccessibility.option_full: l.always_allow = lambda state, item, loc=loc: \ item.game == "SMZ3" and \ loc.alwaysAllow(item.item, state.smz3state[self.player]) @@ -405,12 +407,12 @@ def apply_customization(self): patch = {} # smSpinjumps - if (self.multiworld.spin_jumps_animation[self.player].value == 1): + if (self.options.spin_jumps_animation.value == 1): patch[self.SnesCustomization(0x9B93FE)] = bytearray([0x01]) # z3HeartBeep values = [ 0x00, 0x80, 0x40, 0x20, 0x10] - index = self.multiworld.heart_beep_speed[self.player].value + index = self.options.heart_beep_speed.value patch[0x400033] = bytearray([values[index if index < len(values) else 2]]) # z3HeartColor @@ -420,17 +422,17 @@ def apply_customization(self): [0x2C, [0xC9, 0x69]], [0x28, [0xBC, 0x02]] ] - index = self.multiworld.heart_color[self.player].value + index = self.options.heart_color.value (hud, fileSelect) = values[index if index < len(values) else 0] for i in range(0, 20, 2): patch[self.SnesCustomization(0xDFA1E + i)] = bytearray([hud]) patch[self.SnesCustomization(0x1BD6AA)] = bytearray(fileSelect) # z3QuickSwap - patch[0x40004B] = bytearray([0x01 if self.multiworld.quick_swap[self.player].value else 0x00]) + patch[0x40004B] = bytearray([0x01 if self.options.quick_swap.value else 0x00]) # smEnergyBeepOff - if (self.multiworld.energy_beep[self.player].value == 0): + if (self.options.energy_beep.value == 0): for ([addr, value]) in [ [0x90EA9B, 0x80], [0x90F337, 0x80], @@ -551,7 +553,7 @@ def post_fill(self): # some small or big keys (those always_allow) can be unreachable in-game # while logic still collects some of them (probably to simulate the player collecting pot keys in the logic), some others don't # so we need to remove those exceptions as progression items - if self.multiworld.accessibility[self.player] == 'items': + if self.options.accessibility.value == ItemsAccessibility.option_items: state = CollectionState(self.multiworld) locs = [self.multiworld.get_location("Swamp Palace - Big Chest", self.player), self.multiworld.get_location("Skull Woods - Big Chest", self.player), diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 220b46eae2a4..913fe4b8ad96 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -1,8 +1,8 @@ from ..game_content import ContentPack from ...data import villagers_data, fish_data -from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource +from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource -from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement +from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource from ...strings.book_names import Book from ...strings.crop_names import Fruit @@ -229,8 +229,10 @@ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.mapping_cave_systems: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=(Region.adventurer_guild_bedroom,)), - ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + CompoundSource(sources=( + GenericSource(regions=(Region.adventurer_guild_bedroom,)), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3), + ))), Book.monster_compendium: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)), @@ -248,10 +250,7 @@ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.the_art_o_crabbing: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=(Region.beach,), - other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), - SkillRequirement(Skill.fishing, 6), - SeasonRequirement(Season.winter))), + CustomRuleSource(create_rule=lambda logic: logic.festival.has_squidfest_day_1_iridium_reward()), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.treasure_appraisal_guide: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index 26b1a0d58a81..dfa8891077ee 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -26,6 +26,7 @@ def __repr__(self): fresh_water = (Region.farm, Region.forest, Region.town, Region.mountain) ocean = (Region.beach,) +tide_pools = (Region.tide_pools,) town_river = (Region.town,) mountain_lake = (Region.mountain,) forest_pond = (Region.forest,) @@ -118,13 +119,13 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60) angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False) -crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False) +crimsonfish = create_fish(Fish.crimsonfish, tide_pools, season.summer, 95, True, False) glacierfish = create_fish(Fish.glacierfish, forest_river, season.winter, 100, True, False) legend = create_fish(Fish.legend, mountain_lake, season.spring, 110, True, False) mutant_carp = create_fish(Fish.mutant_carp, sewers, season.all_seasons, 80, True, False) ms_angler = create_fish(Fish.ms_angler, town_river, season.fall, 85, True, True) -son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, ocean, season.summer, 95, True, True) +son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, tide_pools, season.summer, 95, True, True) glacierfish_jr = create_fish(Fish.glacierfish_jr, forest_river, season.winter, 100, True, True) legend_ii = create_fish(Fish.legend_ii, mountain_lake, season.spring, 110, True, True) radioactive_carp = create_fish(Fish.radioactive_carp, sewers, season.all_seasons, 80, True, True) diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py index 2107ca30d33a..6c8d30ed8e6f 100644 --- a/worlds/stardew_valley/data/game_item.py +++ b/worlds/stardew_valley/data/game_item.py @@ -59,6 +59,11 @@ class CustomRuleSource(ItemSource): create_rule: Callable[[Any], StardewRule] +@dataclass(frozen=True, **kw_only) +class CompoundSource(ItemSource): + sources: Tuple[ItemSource, ...] = () + + class Tag(ItemSource): """Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking.""" tag: Tuple[ItemTag, ...] diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index e026090f8659..ffcae223e251 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -474,7 +474,7 @@ id,name,classification,groups,mod_name 507,Resource Pack: 40 Calico Egg,useful,"FESTIVAL", 508,Resource Pack: 35 Calico Egg,useful,"FESTIVAL", 509,Resource Pack: 30 Calico Egg,useful,"FESTIVAL", -510,Book: The Art O' Crabbing,useful,"FESTIVAL", +510,Book: The Art O' Crabbing,progression,"FESTIVAL", 511,Mr Qi's Plane Ride,progression,, 521,Power: Price Catalogue,useful,"BOOK_POWER", 522,Power: Mapping Cave Systems,useful,"BOOK_POWER", @@ -819,6 +819,7 @@ id,name,classification,groups,mod_name 5289,Prismatic Shard,filler,"RESOURCE_PACK", 5290,Stardrop Tea,filler,"RESOURCE_PACK", 5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK", +5292,Resource Pack: 20 Cinder Shard,filler,"GINGER_ISLAND,RESOURCE_PACK", 10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill 10002,Magic Level,progression,SKILL_LEVEL_UP,Magic 10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 608b6a5f576a..680ddfcbacbf 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -313,14 +313,14 @@ id,region,name,tags,mod_name 611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK", 612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART", 613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART", -614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART", +614,Junimo Kart 4,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART", 615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART", 616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART", 617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART", 618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART", 619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART", 620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JOTPK", -621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART", +621,Junimo Kart 4,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART", 701,Secret Woods,Old Master Cannoli,MANDATORY, 702,Beach,Beach Bridge Repair,MANDATORY, 703,Desert,Galaxy Sword Shrine,MANDATORY, diff --git a/worlds/stardew_valley/logic/festival_logic.py b/worlds/stardew_valley/logic/festival_logic.py new file mode 100644 index 000000000000..2b22617202d8 --- /dev/null +++ b/worlds/stardew_valley/logic/festival_logic.py @@ -0,0 +1,186 @@ +from typing import Union + +from .action_logic import ActionLogicMixin +from .animal_logic import AnimalLogicMixin +from .artisan_logic import ArtisanLogicMixin +from .base_logic import BaseLogicMixin, BaseLogic +from .fishing_logic import FishingLogicMixin +from .gift_logic import GiftLogicMixin +from .has_logic import HasLogicMixin +from .money_logic import MoneyLogicMixin +from .monster_logic import MonsterLogicMixin +from .museum_logic import MuseumLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .relationship_logic import RelationshipLogicMixin +from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin +from ..options import FestivalLocations +from ..stardew_rule import StardewRule +from ..strings.book_names import Book +from ..strings.craftable_names import Fishing +from ..strings.crop_names import Fruit, Vegetable +from ..strings.festival_check_names import FestivalCheck +from ..strings.fish_names import Fish +from ..strings.forageable_names import Forageable +from ..strings.generic_names import Generic +from ..strings.machine_names import Machine +from ..strings.monster_names import Monster +from ..strings.region_names import Region + + +class FestivalLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.festival = FestivalLogic(*args, **kwargs) + + +class FestivalLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, FestivalLogicMixin, ArtisanLogicMixin, AnimalLogicMixin, MoneyLogicMixin, TimeLogicMixin, +SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, RelationshipLogicMixin, FishingLogicMixin, MuseumLogicMixin, GiftLogicMixin]]): + + def initialize_rules(self): + self.registry.festival_rules.update({ + FestivalCheck.egg_hunt: self.logic.festival.can_win_egg_hunt(), + FestivalCheck.strawberry_seeds: self.logic.money.can_spend(1000), + FestivalCheck.dance: self.logic.relationship.has_hearts_with_any_bachelor(4), + FestivalCheck.tub_o_flowers: self.logic.money.can_spend(2000), + FestivalCheck.rarecrow_5: self.logic.money.can_spend(2500), + FestivalCheck.luau_soup: self.logic.festival.can_succeed_luau_soup(), + FestivalCheck.moonlight_jellies: self.logic.true_, + FestivalCheck.moonlight_jellies_banner: self.logic.money.can_spend(800), + FestivalCheck.starport_decal: self.logic.money.can_spend(1000), + FestivalCheck.smashing_stone: self.logic.true_, + FestivalCheck.grange_display: self.logic.festival.can_succeed_grange_display(), + FestivalCheck.rarecrow_1: self.logic.true_, # only cost star tokens + FestivalCheck.fair_stardrop: self.logic.true_, # only cost star tokens + FestivalCheck.spirit_eve_maze: self.logic.true_, + FestivalCheck.jack_o_lantern: self.logic.money.can_spend(2000), + FestivalCheck.rarecrow_2: self.logic.money.can_spend(5000), + FestivalCheck.fishing_competition: self.logic.festival.can_win_fishing_competition(), + FestivalCheck.rarecrow_4: self.logic.money.can_spend(5000), + FestivalCheck.mermaid_pearl: self.logic.has(Forageable.secret_note), + FestivalCheck.cone_hat: self.logic.money.can_spend(2500), + FestivalCheck.iridium_fireplace: self.logic.money.can_spend(15000), + FestivalCheck.rarecrow_7: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_artifacts(20), + FestivalCheck.rarecrow_8: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_items(40), + FestivalCheck.lupini_red_eagle: self.logic.money.can_spend(1200), + FestivalCheck.lupini_portrait_mermaid: self.logic.money.can_spend(1200), + FestivalCheck.lupini_solar_kingdom: self.logic.money.can_spend(1200), + FestivalCheck.lupini_clouds: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_1000_years: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_three_trees: self.logic.time.has_year_two & self.logic.money.can_spend(1200), + FestivalCheck.lupini_the_serpent: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.lupini_tropical_fish: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.lupini_land_of_clay: self.logic.time.has_year_three & self.logic.money.can_spend(1200), + FestivalCheck.secret_santa: self.logic.gifts.has_any_universal_love, + FestivalCheck.legend_of_the_winter_star: self.logic.true_, + FestivalCheck.rarecrow_3: self.logic.true_, + FestivalCheck.all_rarecrows: self.logic.region.can_reach(Region.farm) & self.logic.festival.has_all_rarecrows(), + FestivalCheck.calico_race: self.logic.true_, + FestivalCheck.mummy_mask: self.logic.true_, + FestivalCheck.calico_statue: self.logic.true_, + FestivalCheck.emily_outfit_service: self.logic.true_, + FestivalCheck.earthy_mousse: self.logic.true_, + FestivalCheck.sweet_bean_cake: self.logic.true_, + FestivalCheck.skull_cave_casserole: self.logic.true_, + FestivalCheck.spicy_tacos: self.logic.true_, + FestivalCheck.mountain_chili: self.logic.true_, + FestivalCheck.crystal_cake: self.logic.true_, + FestivalCheck.cave_kebab: self.logic.true_, + FestivalCheck.hot_log: self.logic.true_, + FestivalCheck.sour_salad: self.logic.true_, + FestivalCheck.superfood_cake: self.logic.true_, + FestivalCheck.warrior_smoothie: self.logic.true_, + FestivalCheck.rumpled_fruit_skin: self.logic.true_, + FestivalCheck.calico_pizza: self.logic.true_, + FestivalCheck.stuffed_mushrooms: self.logic.true_, + FestivalCheck.elf_quesadilla: self.logic.true_, + FestivalCheck.nachos_of_the_desert: self.logic.true_, + FestivalCheck.cloppino: self.logic.true_, + FestivalCheck.rainforest_shrimp: self.logic.true_, + FestivalCheck.shrimp_donut: self.logic.true_, + FestivalCheck.smell_of_the_sea: self.logic.true_, + FestivalCheck.desert_gumbo: self.logic.true_, + FestivalCheck.free_cactis: self.logic.true_, + FestivalCheck.monster_hunt: self.logic.monster.can_kill(Monster.serpent), + FestivalCheck.deep_dive: self.logic.region.can_reach(Region.skull_cavern_50), + FestivalCheck.treasure_hunt: self.logic.region.can_reach(Region.skull_cavern_25), + FestivalCheck.touch_calico_statue: self.logic.region.can_reach(Region.skull_cavern_25), + FestivalCheck.real_calico_egg_hunter: self.logic.region.can_reach(Region.skull_cavern_100), + FestivalCheck.willy_challenge: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.scorpion_carp]), + FestivalCheck.desert_scholar: self.logic.true_, + FestivalCheck.squidfest_day_1_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_1_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait), + FestivalCheck.squidfest_day_1_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_1_iridium: self.logic.festival.can_squidfest_day_1_iridium_reward(), + FestivalCheck.squidfest_day_2_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_2_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait), + FestivalCheck.squidfest_day_2_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_2_iridium: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & + self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]), + }) + for i in range(1, 11): + check_name = f"{FestivalCheck.trout_derby_reward_pattern}{i}" + self.registry.festival_rules[check_name] = self.logic.fishing.can_catch_fish(self.content.fishes[Fish.rainbow_trout]) + + def can_squidfest_day_1_iridium_reward(self) -> StardewRule: + return self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]) + + def has_squidfest_day_1_iridium_reward(self) -> StardewRule: + if self.options.festival_locations == FestivalLocations.option_disabled: + return self.logic.festival.can_squidfest_day_1_iridium_reward() + else: + return self.logic.received(f"Book: {Book.the_art_o_crabbing}") + + def can_win_egg_hunt(self) -> StardewRule: + return self.logic.true_ + + def can_succeed_luau_soup(self) -> StardewRule: + if self.options.festival_locations != FestivalLocations.option_hard: + return self.logic.true_ + eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, + Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber) + fish_rule = self.logic.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray + eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, + Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry, + Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum, + Vegetable.hops, Vegetable.wheat) + keg_rules = [self.logic.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items] + aged_rule = self.logic.has(Machine.cask) & self.logic.or_(*keg_rules) + # There are a few other valid items, but I don't feel like coding them all + return fish_rule | aged_rule + + def can_succeed_grange_display(self) -> StardewRule: + if self.options.festival_locations != FestivalLocations.option_hard: + return self.logic.true_ + + animal_rule = self.logic.animal.has_animal(Generic.any) + artisan_rule = self.logic.artisan.can_keg(Generic.any) | self.logic.artisan.can_preserves_jar(Generic.any) + cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough + fish_rule = self.logic.skill.can_fish(difficulty=50) + forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall + mineral_rule = self.logic.action.can_open_geode(Generic.any) # More than half the minerals are good enough + good_fruits = (fruit + for fruit in + (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, + Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit) + if fruit in self.content.game_items) + fruit_rule = self.logic.has_any(*good_fruits) + good_vegetables = (vegeteable + for vegeteable in + (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, + Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin) + if vegeteable in self.content.game_items) + vegetable_rule = self.logic.has_any(*good_vegetables) + + return animal_rule & artisan_rule & cooking_rule & fish_rule & \ + forage_rule & fruit_rule & mineral_rule & vegetable_rule + + def can_win_fishing_competition(self) -> StardewRule: + return self.logic.skill.can_fish(difficulty=60) + + def has_all_rarecrows(self) -> StardewRule: + rules = [] + for rarecrow_number in range(1, 9): + rules.append(self.logic.received(f"Rarecrow #{rarecrow_number}")) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py index ccd8c5daccfb..e0ac84639d9c 100644 --- a/worlds/stardew_valley/logic/grind_logic.py +++ b/worlds/stardew_valley/logic/grind_logic.py @@ -5,6 +5,7 @@ from .book_logic import BookLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin from ..options import Booksanity from ..stardew_rule import StardewRule, HasProgressionPercent @@ -13,6 +14,7 @@ from ..strings.currency_names import Currency from ..strings.fish_names import WaterChest from ..strings.geode_names import Geode +from ..strings.region_names import Region from ..strings.tool_names import Tool if TYPE_CHECKING: @@ -31,26 +33,28 @@ def __init__(self, *args, **kwargs): self.grind = GrindLogic(*args, **kwargs) -class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): +class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): def can_grind_mystery_boxes(self, quantity: int) -> StardewRule: + opening_rule = self.logic.region.can_reach(Region.blacksmith) mystery_box_rule = self.logic.has(Consumable.mystery_box) book_of_mysteries_rule = self.logic.true_ \ if self.options.booksanity == Booksanity.option_none \ else self.logic.book.has_book_power(Book.book_of_mysteries) # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride. time_rule = self.logic.time.has_lived_months(quantity // 14) - return self.logic.and_(mystery_box_rule, - book_of_mysteries_rule, - time_rule) + return self.logic.and_(opening_rule, mystery_box_rule, + book_of_mysteries_rule, time_rule,) def can_grind_artifact_troves(self, quantity: int) -> StardewRule: - return self.logic.and_(self.logic.has(Geode.artifact_trove), + opening_rule = self.logic.region.can_reach(Region.blacksmith) + return self.logic.and_(opening_rule, self.logic.has(Geode.artifact_trove), # Assuming one per month if the player does not grind it. self.logic.time.has_lived_months(quantity)) def can_grind_prize_tickets(self, quantity: int) -> StardewRule: - return self.logic.and_(self.logic.has(Currency.prize_ticket), + claiming_rule = self.logic.region.can_reach(Region.mayor_house) + return self.logic.and_(claiming_rule, self.logic.has(Currency.prize_ticket), # Assuming two per month if the player does not grind it. self.logic.time.has_lived_months(quantity // 2)) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index fb0d938fbb1e..9d4447439f7b 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -16,6 +16,7 @@ from .cooking_logic import CookingLogicMixin from .crafting_logic import CraftingLogicMixin from .farming_logic import FarmingLogicMixin +from .festival_logic import FestivalLogicMixin from .fishing_logic import FishingLogicMixin from .gift_logic import GiftLogicMixin from .grind_logic import GrindLogicMixin @@ -62,7 +63,6 @@ from ..strings.currency_names import Currency from ..strings.decoration_names import Decoration from ..strings.fertilizer_names import Fertilizer, SpeedGro, RetainingSoil -from ..strings.festival_check_names import FestivalCheck from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest from ..strings.flower_names import Flower from ..strings.food_names import Meal, Beverage @@ -94,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, - RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin): + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions content: StardewContent @@ -363,89 +363,7 @@ def __init__(self, player: int, options: StardewValleyOptions, content: StardewC self.quest.initialize_rules() self.quest.update_rules(self.mod.quest.get_modded_quest_rules()) - self.registry.festival_rules.update({ - FestivalCheck.egg_hunt: self.can_win_egg_hunt(), - FestivalCheck.strawberry_seeds: self.money.can_spend(1000), - FestivalCheck.dance: self.relationship.has_hearts_with_any_bachelor(4), - FestivalCheck.tub_o_flowers: self.money.can_spend(2000), - FestivalCheck.rarecrow_5: self.money.can_spend(2500), - FestivalCheck.luau_soup: self.can_succeed_luau_soup(), - FestivalCheck.moonlight_jellies: True_(), - FestivalCheck.moonlight_jellies_banner: self.money.can_spend(800), - FestivalCheck.starport_decal: self.money.can_spend(1000), - FestivalCheck.smashing_stone: True_(), - FestivalCheck.grange_display: self.can_succeed_grange_display(), - FestivalCheck.rarecrow_1: True_(), # only cost star tokens - FestivalCheck.fair_stardrop: True_(), # only cost star tokens - FestivalCheck.spirit_eve_maze: True_(), - FestivalCheck.jack_o_lantern: self.money.can_spend(2000), - FestivalCheck.rarecrow_2: self.money.can_spend(5000), - FestivalCheck.fishing_competition: self.can_win_fishing_competition(), - FestivalCheck.rarecrow_4: self.money.can_spend(5000), - FestivalCheck.mermaid_pearl: self.has(Forageable.secret_note), - FestivalCheck.cone_hat: self.money.can_spend(2500), - FestivalCheck.iridium_fireplace: self.money.can_spend(15000), - FestivalCheck.rarecrow_7: self.money.can_spend(5000) & self.museum.can_donate_museum_artifacts(20), - FestivalCheck.rarecrow_8: self.money.can_spend(5000) & self.museum.can_donate_museum_items(40), - FestivalCheck.lupini_red_eagle: self.money.can_spend(1200), - FestivalCheck.lupini_portrait_mermaid: self.money.can_spend(1200), - FestivalCheck.lupini_solar_kingdom: self.money.can_spend(1200), - FestivalCheck.lupini_clouds: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_1000_years: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_three_trees: self.time.has_year_two & self.money.can_spend(1200), - FestivalCheck.lupini_the_serpent: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.lupini_tropical_fish: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.lupini_land_of_clay: self.time.has_year_three & self.money.can_spend(1200), - FestivalCheck.secret_santa: self.gifts.has_any_universal_love, - FestivalCheck.legend_of_the_winter_star: True_(), - FestivalCheck.rarecrow_3: True_(), - FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(), - FestivalCheck.calico_race: True_(), - FestivalCheck.mummy_mask: True_(), - FestivalCheck.calico_statue: True_(), - FestivalCheck.emily_outfit_service: True_(), - FestivalCheck.earthy_mousse: True_(), - FestivalCheck.sweet_bean_cake: True_(), - FestivalCheck.skull_cave_casserole: True_(), - FestivalCheck.spicy_tacos: True_(), - FestivalCheck.mountain_chili: True_(), - FestivalCheck.crystal_cake: True_(), - FestivalCheck.cave_kebab: True_(), - FestivalCheck.hot_log: True_(), - FestivalCheck.sour_salad: True_(), - FestivalCheck.superfood_cake: True_(), - FestivalCheck.warrior_smoothie: True_(), - FestivalCheck.rumpled_fruit_skin: True_(), - FestivalCheck.calico_pizza: True_(), - FestivalCheck.stuffed_mushrooms: True_(), - FestivalCheck.elf_quesadilla: True_(), - FestivalCheck.nachos_of_the_desert: True_(), - FestivalCheck.cloppino: True_(), - FestivalCheck.rainforest_shrimp: True_(), - FestivalCheck.shrimp_donut: True_(), - FestivalCheck.smell_of_the_sea: True_(), - FestivalCheck.desert_gumbo: True_(), - FestivalCheck.free_cactis: True_(), - FestivalCheck.monster_hunt: self.monster.can_kill(Monster.serpent), - FestivalCheck.deep_dive: self.region.can_reach(Region.skull_cavern_50), - FestivalCheck.treasure_hunt: self.region.can_reach(Region.skull_cavern_25), - FestivalCheck.touch_calico_statue: self.region.can_reach(Region.skull_cavern_25), - FestivalCheck.real_calico_egg_hunter: self.region.can_reach(Region.skull_cavern_100), - FestivalCheck.willy_challenge: self.fishing.can_catch_fish(content.fishes[Fish.scorpion_carp]), - FestivalCheck.desert_scholar: True_(), - FestivalCheck.squidfest_day_1_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), - FestivalCheck.squidfest_day_1_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), - FestivalCheck.squidfest_day_1_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), - FestivalCheck.squidfest_day_1_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & - self.fishing.has_specific_bait(content.fishes[Fish.squid]), - FestivalCheck.squidfest_day_2_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), - FestivalCheck.squidfest_day_2_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), - FestivalCheck.squidfest_day_2_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), - FestivalCheck.squidfest_day_2_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & - self.fishing.has_specific_bait(content.fishes[Fish.squid]), - }) - for i in range(1, 11): - self.registry.festival_rules[f"{FestivalCheck.trout_derby_reward_pattern}{i}"] = self.fishing.can_catch_fish(content.fishes[Fish.rainbow_trout]) + self.festival.initialize_rules() self.special_order.initialize_rules() self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules()) @@ -486,53 +404,6 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: ] return self.count(12, *rules_worth_a_point) - def can_win_egg_hunt(self) -> StardewRule: - return True_() - - def can_succeed_luau_soup(self) -> StardewRule: - if self.options.festival_locations != FestivalLocations.option_hard: - return True_() - eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, - Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber) - fish_rule = self.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray - eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, - Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry, - Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum, - Vegetable.hops, Vegetable.wheat) - keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items] - aged_rule = self.has(Machine.cask) & self.logic.or_(*keg_rules) - # There are a few other valid items, but I don't feel like coding them all - return fish_rule | aged_rule - - def can_succeed_grange_display(self) -> StardewRule: - if self.options.festival_locations != FestivalLocations.option_hard: - return True_() - - animal_rule = self.animal.has_animal(Generic.any) - artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any) - cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough - fish_rule = self.skill.can_fish(difficulty=50) - forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall - mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough - good_fruits = (fruit - for fruit in - (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, - Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit) - if fruit in self.content.game_items) - fruit_rule = self.has_any(*good_fruits) - good_vegetables = (vegeteable - for vegeteable in - (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, - Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin) - if vegeteable in self.content.game_items) - vegetable_rule = self.has_any(*good_vegetables) - - return animal_rule & artisan_rule & cooking_rule & fish_rule & \ - forage_rule & fruit_rule & mineral_rule & vegetable_rule - - def can_win_fishing_competition(self) -> StardewRule: - return self.skill.can_fish(difficulty=60) - def has_island_trader(self) -> StardewRule: if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() @@ -571,12 +442,6 @@ def has_all_stardrops(self) -> StardewRule: return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules) - def has_all_rarecrows(self) -> StardewRule: - rules = [] - for rarecrow_number in range(1, 9): - rules.append(self.received(f"Rarecrow #{rarecrow_number}")) - return self.logic.and_(*rules) - def has_abandoned_jojamart(self) -> StardewRule: return self.received(CommunityUpgrade.movie_theater, 1) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 4d5567302afe..17fabca28d95 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -15,13 +15,13 @@ from ..data.harvest import HarvestCropSource from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_skills_levels import get_mod_skill_levels -from ..stardew_rule import StardewRule, True_, False_, true_, And +from ..stardew_rule import StardewRule, true_, True_, False_ from ..strings.craftable_names import Fishing from ..strings.machine_names import Machine from ..strings.performance_names import Performance from ..strings.quality_names import ForageQuality from ..strings.region_names import Region -from ..strings.skill_names import Skill, all_mod_skills +from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills from ..strings.tool_names import ToolMaterial, Tool from ..strings.wallet_item_names import Wallet @@ -43,22 +43,17 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() - tool_level = (level - 1) // 2 + tool_level = min(4, (level - 1) // 2) tool_material = ToolMaterial.tiers[tool_level] - months = max(1, level - 1) - months_rule = self.logic.time.has_lived_months(months) - if self.options.skill_progression != options.SkillProgression.option_vanilla: - previous_level_rule = self.logic.skill.has_level(skill, level - 1) - else: - previous_level_rule = true_ + previous_level_rule = self.logic.skill.has_previous_level(skill, level) if skill == Skill.fishing: xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) elif skill == Skill.farming: xp_rule = self.can_get_farming_xp & self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) elif skill == Skill.foraging: - xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\ + xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) | \ self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) elif skill == Skill.mining: xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \ @@ -70,22 +65,34 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5) elif skill in all_mod_skills: # Ideal solution would be to add a logic registry, but I'm too lazy. - return previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) + return previous_level_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) else: raise Exception(f"Unknown skill: {skill}") - return previous_level_rule & months_rule & xp_rule + return previous_level_rule & xp_rule # Should be cached def has_level(self, skill: str, level: int) -> StardewRule: - if level <= 0: - return True_() + assert level >= 0, f"There is no level before level 0." + if level == 0: + return true_ if self.options.skill_progression == options.SkillProgression.option_vanilla: return self.logic.skill.can_earn_level(skill, level) return self.logic.received(f"{skill} Level", level) + def has_previous_level(self, skill: str, level: int) -> StardewRule: + assert level > 0, f"There is no level before level 0." + if level == 1: + return true_ + + if self.options.skill_progression == options.SkillProgression.option_vanilla: + months = max(1, level - 1) + return self.logic.time.has_lived_months(months) + + return self.logic.received(f"{skill} Level", level - 1) + @cache_self1 def has_farming_level(self, level: int) -> StardewRule: return self.logic.skill.has_level(Skill.farming, level) @@ -108,18 +115,9 @@ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> Star return rule_with_fishing return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing - def has_all_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_vanilla: - return self.has_total_level(50) - skills_items = vanilla_skill_items - if included_modded_skills: - skills_items += get_mod_skill_levels(self.options.mods) - return And(*[self.logic.received(skill, 10) for skill in skills_items]) - - def can_enter_mastery_cave(self) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: - return self.logic.received(Wallet.mastery_of_the_five_ways) - return self.has_all_skills_maxed() + def has_any_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: + skills = self.content.skills.keys() if included_modded_skills else sorted(all_vanilla_skills) + return self.logic.or_(*(self.logic.skill.has_level(skill, 10) for skill in skills)) @cached_property def can_get_farming_xp(self) -> StardewRule: @@ -197,13 +195,19 @@ def can_forage_quality(self, quality: str) -> StardewRule: return self.has_level(Skill.foraging, 9) return False_() - @cached_property - def can_earn_mastery_experience(self) -> StardewRule: - if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: - return self.has_all_skills_maxed() & self.logic.time.has_lived_max_months - return self.logic.time.has_lived_max_months + def can_earn_mastery(self, skill: str) -> StardewRule: + # Checking for level 11, so it includes having level 10 and being able to earn xp. + return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave) def has_mastery(self, skill: str) -> StardewRule: - if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: - return self.can_earn_mastery_experience and self.logic.region.can_reach(Region.mastery_cave) - return self.logic.received(f"{skill} Mastery") + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(f"{skill} Mastery") + + return self.logic.skill.can_earn_mastery(skill) + + @cached_property + def can_enter_mastery_cave(self) -> StardewRule: + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(Wallet.mastery_of_the_five_ways) + + return self.has_any_skills_maxed(included_modded_skills=False) diff --git a/worlds/stardew_valley/logic/source_logic.py b/worlds/stardew_valley/logic/source_logic.py index 0e9b8e976f5b..9ef68a020eef 100644 --- a/worlds/stardew_valley/logic/source_logic.py +++ b/worlds/stardew_valley/logic/source_logic.py @@ -12,7 +12,7 @@ from .requirement_logic import RequirementLogicMixin from .tool_logic import ToolLogicMixin from ..data.artisan import MachineSource -from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource +from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource, CompoundSource from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \ HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource @@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs): class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin, -ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): + ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): def has_access_to_item(self, item: GameItem): rules = [] @@ -40,6 +40,10 @@ def has_access_to_any(self, sources: Iterable[ItemSource]): return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements) for source in sources)) + def has_access_to_all(self, sources: Iterable[ItemSource]): + return self.logic.and_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements) + for source in sources)) + @functools.singledispatchmethod def has_access_to(self, source: Any): raise ValueError(f"Sources of type{type(source)} have no rule registered.") @@ -52,6 +56,10 @@ def _(self, source: GenericSource): def _(self, source: CustomRuleSource): return source.create_rule(self.logic) + @has_access_to.register + def _(self, source: CompoundSource): + return self.logic.source.has_access_to_all(source.sources) + @has_access_to.register def _(self, source: ForagingSource): return self.logic.harvesting.can_forage_from(source) diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index cf6f87a1501c..1861a914235c 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -57,8 +57,6 @@ } easy_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "very rich", @@ -103,8 +101,6 @@ } medium_settings = { - "progression_balancing": 25, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "rich", @@ -149,8 +145,6 @@ } hard_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_grandpa_evaluation, FarmType.internal_name: "random", StartingMoney.internal_name: "extra", @@ -195,8 +189,6 @@ } nightmare_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "vanilla", @@ -241,8 +233,6 @@ } short_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_bottom_of_the_mines, FarmType.internal_name: "random", StartingMoney.internal_name: "filthy rich", @@ -287,8 +277,6 @@ } minsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_minimal, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, @@ -333,8 +321,6 @@ } allsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_full, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index b0fc7fa0ea52..5b7db5ac79d1 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -87,7 +87,8 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.jotpk_world_3), RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]), RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]), - RegionData(Region.junimo_kart_3), + RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]), + RegionData(Region.junimo_kart_4), RegionData(Region.alex_house), RegionData(Region.trailer), RegionData(Region.mayor_house), @@ -330,6 +331,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1), ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2), ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3), + ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4), ConnectionData(Entrance.town_to_sam_house, Region.sam_house, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.town_to_haley_house, Region.haley_house, diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 89b1cf87c3c1..eda2d4377e09 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -39,6 +39,7 @@ from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \ SVEEntrance, LaceyEntrance, BoardingHouseEntrance, LogicEntrance from .strings.forageable_names import Forageable +from .strings.generic_names import Generic from .strings.geode_names import Geode from .strings.material_names import Material from .strings.metal_names import MetalBar, Mineral @@ -154,7 +155,7 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw extra_raccoons = extra_raccoons + num bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules if num > 1: - previous_bundle_name = f"Raccoon Request {num-1}" + previous_bundle_name = f"Raccoon Request {num - 1}" bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name) room_rules.append(bundle_rules) MultiWorldRules.set_rule(location, bundle_rules) @@ -168,13 +169,16 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta mods = world_options.mods if world_options.skill_progression == SkillProgression.option_vanilla: return + for i in range(1, 11): set_vanilla_skill_rule_for_level(logic, multiworld, player, i) set_modded_skill_rule_for_level(logic, multiworld, player, mods, i) - if world_options.skill_progression != SkillProgression.option_progressive_with_masteries: + + if world_options.skill_progression == SkillProgression.option_progressive: return + for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]: - MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery_experience) + MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill)) def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): @@ -256,11 +260,11 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, LogicEntrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen) set_entrance_rule(multiworld, player, LogicEntrance.shipping, logic.shipping.can_use_shipping_bin) set_entrance_rule(multiworld, player, LogicEntrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce)) - set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) - set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) + set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave) set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2)) set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two) set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three) + set_entrance_rule(multiworld, player, Entrance.adventurer_guild_to_bedroom, logic.monster.can_kill_max(Generic.any)) def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewValleyOptions): @@ -887,7 +891,7 @@ def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player logic.has("Junimo Kart Medium Buff")) MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player), logic.has("Junimo Kart Big Buff")) - MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player), + MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_4, player), logic.has("Junimo Kart Max Buff")) MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player), logic.has("JotPK Small Buff")) diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 58a919f2a8a4..b1c84004eb7a 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -94,6 +94,7 @@ class Entrance: play_junimo_kart = "Play Junimo Kart" reach_junimo_kart_2 = "Reach Junimo Kart 2" reach_junimo_kart_3 = "Reach Junimo Kart 3" + reach_junimo_kart_4 = "Reach Junimo Kart 4" enter_locker_room = "Bathhouse Entrance to Locker Room" enter_public_bath = "Locker Room to Public Bath" enter_witch_swamp = "Witch Warp Cave to Witch's Swamp" diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 58763b6fcb80..2bbc6228ab19 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -114,6 +114,7 @@ class Region: junimo_kart_1 = "Junimo Kart 1" junimo_kart_2 = "Junimo Kart 2" junimo_kart_3 = "Junimo Kart 3" + junimo_kart_4 = "Junimo Kart 4" mines_floor_5 = "The Mines - Floor 5" mines_floor_10 = "The Mines - Floor 10" mines_floor_15 = "The Mines - Floor 15" diff --git a/worlds/stardew_valley/test/TestDynamicGoals.py b/worlds/stardew_valley/test/TestDynamicGoals.py index bfa58dd34063..b0e6d6c62655 100644 --- a/worlds/stardew_valley/test/TestDynamicGoals.py +++ b/worlds/stardew_valley/test/TestDynamicGoals.py @@ -27,6 +27,7 @@ def collect_fishing_abilities(tester: SVTestBase): tester.multiworld.state.collect(tester.world.create_item("Fall"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Winter"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Beach Bridge"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), prevent_sweep=False) tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), prevent_sweep=False) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index c2c2a6a20baf..3fe05d205ce0 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -85,7 +85,7 @@ def allsanity_no_mods_6_x_x(): options.QuestLocations.internal_name: 56, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, options.ToolProgression.internal_name: options.ToolProgression.option_progressive, options.TrapItems.internal_name: options.TrapItems.option_nightmare, @@ -256,17 +256,21 @@ def run_default_tests(self) -> bool: return False return super().run_default_tests - def collect_lots_of_money(self): + def collect_lots_of_money(self, percent: float = 0.25): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) - required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25)) + real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items + required_prog_items = int(round(real_total_prog_items * percent)) for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) + self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items def collect_all_the_money(self): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) - required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95)) + real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items + required_prog_items = int(round(real_total_prog_items * 0.95)) for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) + self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items def collect_everything(self): non_event_items = [item for item in self.multiworld.get_items() if item.code] @@ -306,6 +310,12 @@ def create_item(self, item: str) -> StardewItem: self.multiworld.worlds[self.player].total_progression_items -= 1 return created_item + def remove_one_by_name(self, item: str) -> None: + self.remove(self.create_item(item)) + + def reset_collection_state(self): + self.multiworld.state = self.original_state.copy() + pre_generated_worlds = {} diff --git a/worlds/stardew_valley/test/rules/TestBooks.py b/worlds/stardew_valley/test/rules/TestBooks.py new file mode 100644 index 000000000000..6605e7e645e3 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBooks.py @@ -0,0 +1,26 @@ +from ... import options +from ...test import SVTestBase + + +class TestBooksLogic(SVTestBase): + options = { + options.Booksanity.internal_name: options.Booksanity.option_all, + } + + def test_need_weapon_for_mapping_cave_systems(self): + self.collect_lots_of_money(0.5) + + location = self.multiworld.get_location("Read Mapping Cave Systems", self.player) + + self.assert_reach_location_false(location, self.multiworld.state) + + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.collect("Progressive Mine Elevator") + self.assert_reach_location_false(location, self.multiworld.state) + + self.collect("Progressive Weapon") + self.assert_reach_location_true(location, self.multiworld.state) + + diff --git a/worlds/stardew_valley/test/rules/TestFishing.py b/worlds/stardew_valley/test/rules/TestFishing.py new file mode 100644 index 000000000000..04a1528dd8b1 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestFishing.py @@ -0,0 +1,61 @@ +from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \ + ElevatorProgression, SpecialOrderLocations +from ...strings.fish_names import Fish +from ...test import SVTestBase + + +class TestNeedRegionToCatchFish(SVTestBase): + options = { + SeasonRandomization.internal_name: SeasonRandomization.option_disabled, + ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, + SkillProgression.internal_name: SkillProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_vanilla, + Fishsanity.internal_name: Fishsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + } + + def test_catch_fish_requires_region_unlock(self): + fish_and_items = { + Fish.crimsonfish: ["Beach Bridge"], + Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"], + Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades + Fish.mutant_carp: ["Rusty Key"], + Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"], + Fish.lionfish: ["Boat Repair"], + Fish.blue_discus: ["Island Obelisk", "Island West Turtle"], + Fish.stingray: ["Boat Repair", "Island Resort"], + Fish.ghostfish: ["Progressive Weapon"], + Fish.stonefish: ["Progressive Weapon"], + Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"], + Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"], + Fish.sandfish: ["Bus Repair"], + Fish.scorpion_carp: ["Desert Obelisk"], + # Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary + Fish.son_of_crimsonfish: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + Fish.radioactive_carp: ["Beach Bridge", "Rusty Key", "Boat Repair", "Island West Turtle", "Qi Walnut Room"], + Fish.glacierfish_jr: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"], + } + self.original_state = self.multiworld.state.copy() + for fish in fish_and_items: + with self.subTest(f"Region rules for {fish}"): + self.collect_all_the_money() + item_names = fish_and_items[fish] + location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + items = [] + for item_name in item_names: + items.append(self.collect(item_name)) + with self.subTest(f"{fish} can be reached with {item_names}"): + self.assert_reach_location_true(location, self.multiworld.state) + for item_required in items: + self.multiworld.state = self.original_state.copy() + with self.subTest(f"{fish} requires {item_required.name}"): + for item_to_collect in items: + if item_to_collect.name != item_required.name: + self.collect(item_to_collect) + self.assert_reach_location_false(location, self.multiworld.state) + + self.multiworld.state = self.original_state.copy() diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py index 1c6874f31529..77adade886dc 100644 --- a/worlds/stardew_valley/test/rules/TestSkills.py +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -1,23 +1,30 @@ -from ... import HasProgressionPercent +from ... import HasProgressionPercent, StardewLogic from ...options import ToolProgression, SkillProgression, Mods -from ...strings.skill_names import all_skills +from ...strings.skill_names import all_skills, all_vanilla_skills, Skill from ...test import SVTestBase -class TestVanillaSkillLogicSimplification(SVTestBase): +class TestSkillProgressionVanilla(SVTestBase): options = { SkillProgression.internal_name: SkillProgression.option_vanilla, ToolProgression.internal_name: ToolProgression.option_progressive, } def test_skill_logic_has_level_only_uses_one_has_progression_percent(self): - rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8) - self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent)) + rule = self.multiworld.worlds[1].logic.skill.has_level(Skill.farming, 8) + self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) is HasProgressionPercent)) + def test_has_mastery_requires_month_equivalent_to_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + time_rule = logic.time.has_lived_months(10) -class TestAllSkillsRequirePrevious(SVTestBase): + self.assertIn(time_rule, rule.current_rules) + + +class TestSkillProgressionProgressive(SVTestBase): options = { - SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + SkillProgression.internal_name: SkillProgression.option_progressive, Mods.internal_name: frozenset(Mods.valid_keys), } @@ -25,16 +32,82 @@ def test_all_skill_levels_require_previous_level(self): for skill in all_skills: self.collect_everything() self.remove_by_name(f"{skill} Level") + for level in range(1, 11): location_name = f"Level {level} {skill}" + location = self.multiworld.get_location(location_name, self.player) + with self.subTest(location_name): - can_reach = self.can_reach_location(location_name) if level > 1: - self.assertFalse(can_reach) + self.assert_reach_location_false(location, self.multiworld.state) self.collect(f"{skill} Level") - can_reach = self.can_reach_location(location_name) - self.assertTrue(can_reach) - self.multiworld.state = self.original_state.copy() + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_has_level_requires_exact_amount_of_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 8) + + self.assertEqual(level_rule, rule) + + def test_has_previous_level_requires_one_less_level_than_requested(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_previous_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 7) + + self.assertEqual(level_rule, rule) + + def test_has_mastery_requires_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + level_rule = logic.received("Farming Level", 10) + + self.assertIn(level_rule, rule.current_rules) + + +class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + ToolProgression.internal_name: ToolProgression.option_progressive, + Mods.internal_name: frozenset(), + } + + def test_has_mastery_requires_the_item(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + received_mastery = logic.received("Farming Mastery") + + self.assertEqual(received_mastery, rule) + + def test_given_all_levels_when_can_earn_mastery_then_can_earn_mastery(self): + self.collect_everything() + + for skill in all_vanilla_skills: + with self.subTest(skill): + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_level_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + for skill in all_vanilla_skills: + with self.subTest(skill): + self.collect_everything() + self.remove_one_by_name(f"{skill} Level") + + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_tool_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + self.collect_everything() + self.remove_one_by_name(f"Progressive Pickaxe") + location = self.multiworld.get_location("Mining Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + self.reset_collection_state() diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 58d8fa543a6d..c3cf40a7c010 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -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 @@ def generate_early(self) -> None: 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 @@ def create_regions(self): # 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 diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index f99dd7615571..2423e06bb010 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -207,7 +207,7 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)), # 1337158 Is lost in time - LocationData('Library', 'Library: Terminal 3 (Emporer Nuvius)', 1337159, lambda state: state.has('Tablet', player)), + LocationData('Library', 'Library: Terminal 3 (Emperor Nuvius)', 1337159, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: V terminal 1 (War of the Sisters)', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library: V terminal 2 (Lake Desolation Map)', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), LocationData('Library', 'Library: V terminal 3 (Vilete)', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 47c66591f912..cdd968acce44 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -7,8 +7,9 @@ from .er_rules import set_er_location_rules from .regions import tunic_regions from .er_scripts import create_er_regions -from .er_data import portal_mapping -from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections +from .er_data import portal_mapping, RegionInfo, tunic_er_regions +from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections, + LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage) from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -48,10 +49,12 @@ class TunicLocation(Location): class SeedGroup(TypedDict): - logic_rules: int # logic rules value + laurels_zips: bool # laurels_zips value + ice_grappling: int # ice_grappling value + ladder_storage: int # ls value laurels_at_10_fairies: bool # laurels location value fixed_shop: bool # fixed shop value - plando: TunicPlandoConnections # consolidated of plando connections for the seed group + plando: TunicPlandoConnections # consolidated plando connections for the seed group class TunicWorld(World): @@ -77,8 +80,17 @@ class TunicWorld(World): tunic_portal_pairs: Dict[str, str] er_portal_hints: Dict[int, str] seed_groups: Dict[str, SeedGroup] = {} + shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected + er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work def generate_early(self) -> None: + if self.options.logic_rules >= LogicRules.option_no_major_glitches: + self.options.laurels_zips.value = LaurelsZips.option_true + self.options.ice_grappling.value = IceGrappling.option_medium + if self.options.logic_rules.value == LogicRules.option_unrestricted: + self.options.ladder_storage.value = LadderStorage.option_medium + + self.er_regions = tunic_er_regions.copy() if self.options.plando_connections: for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later @@ -99,7 +111,10 @@ def generate_early(self) -> None: self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"] self.options.sword_progression.value = passthrough["sword_progression"] self.options.ability_shuffling.value = passthrough["ability_shuffling"] - self.options.logic_rules.value = passthrough["logic_rules"] + self.options.laurels_zips.value = passthrough["laurels_zips"] + self.options.ice_grappling.value = passthrough["ice_grappling"] + self.options.ladder_storage.value = passthrough["ladder_storage"] + self.options.ladder_storage_without_items = passthrough["ladder_storage_without_items"] self.options.lanternless.value = passthrough["lanternless"] self.options.maskless.value = passthrough["maskless"] self.options.hexagon_quest.value = passthrough["hexagon_quest"] @@ -118,19 +133,28 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: group = tunic.options.entrance_rando.value # if this is the first world in the group, set the rules equal to its rules if group not in cls.seed_groups: - cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value, - laurels_at_10_fairies=tunic.options.laurels_location == 3, - fixed_shop=bool(tunic.options.fixed_shop), - plando=tunic.options.plando_connections) + cls.seed_groups[group] = \ + SeedGroup(laurels_zips=bool(tunic.options.laurels_zips), + ice_grappling=tunic.options.ice_grappling.value, + ladder_storage=tunic.options.ladder_storage.value, + laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies, + fixed_shop=bool(tunic.options.fixed_shop), + plando=tunic.options.plando_connections) continue - + + # off is more restrictive + if not tunic.options.laurels_zips: + cls.seed_groups[group]["laurels_zips"] = False + # lower value is more restrictive + if tunic.options.ice_grappling < cls.seed_groups[group]["ice_grappling"]: + cls.seed_groups[group]["ice_grappling"] = tunic.options.ice_grappling.value # lower value is more restrictive - if tunic.options.logic_rules.value < cls.seed_groups[group]["logic_rules"]: - cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value + if tunic.options.ladder_storage.value < cls.seed_groups[group]["ladder_storage"]: + cls.seed_groups[group]["ladder_storage"] = tunic.options.ladder_storage.value # laurels at 10 fairies changes logic for secret gathering place placement if tunic.options.laurels_location == 3: cls.seed_groups[group]["laurels_at_10_fairies"] = True - # fewer shops, one at windmill + # more restrictive, overrides the option for others in the same group, which is better than failing imo if tunic.options.fixed_shop: cls.seed_groups[group]["fixed_shop"] = True @@ -339,7 +363,8 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: except KeyError: # logic bug, proceed with warning since it takes a long time to update AP warning(f"{location.name} is not logically accessible for {self.player_name}. " - "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs.") + "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. " + "If you are using Plando Items (excluding early locations), then this is likely the cause.") hint_text = "Inaccessible" else: while connection != ("Menu", None): @@ -365,7 +390,10 @@ def fill_slot_data(self) -> Dict[str, Any]: "ability_shuffling": self.options.ability_shuffling.value, "hexagon_quest": self.options.hexagon_quest.value, "fool_traps": self.options.fool_traps.value, - "logic_rules": self.options.logic_rules.value, + "laurels_zips": self.options.laurels_zips.value, + "ice_grappling": self.options.ice_grappling.value, + "ladder_storage": self.options.ladder_storage.value, + "ladder_storage_without_items": self.options.ladder_storage_without_items.value, "lanternless": self.options.lanternless.value, "maskless": self.options.maskless.value, "entrance_rando": int(bool(self.options.entrance_rando.value)), diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 27df4ce38be4..b2e1a71897c0 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -83,8 +83,6 @@ Notes: - The `direction` field is not supported. Connections are always coupled. - For a list of entrance names, check `er_data.py` in the TUNIC world folder or generate a game with the Entrance Randomizer option enabled and check the spoiler log. - There is no limit to the number of Shops you can plando. -- If you have more than one shop in a scene, you may be wrong warped when exiting a shop. -- If you have a shop in every scene, and you have an odd number of shops, it will error out. See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando. diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 6316292e564e..343bf3055378 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1,6 +1,9 @@ -from typing import Dict, NamedTuple, List +from typing import Dict, NamedTuple, List, TYPE_CHECKING, Optional from enum import IntEnum +if TYPE_CHECKING: + from . import TunicWorld + class Portal(NamedTuple): name: str # human-readable name @@ -9,6 +12,8 @@ class Portal(NamedTuple): tag: str # vanilla tag def scene(self) -> str: # the actual scene name in Tunic + if self.region.startswith("Shop"): + return tunic_er_regions["Shop"].game_scene return tunic_er_regions[self.region].game_scene def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod @@ -458,7 +463,7 @@ def destination_scene(self) -> str: # the vanilla connection Portal(name="Cathedral Main Exit", region="Cathedral", destination="Swamp Redux 2", tag="_main"), - Portal(name="Cathedral Elevator", region="Cathedral", + Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet", destination="Cathedral Arena", tag="_"), Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", destination="Swamp Redux 2", tag="_secret"), @@ -517,6 +522,13 @@ def destination_scene(self) -> str: # the vanilla connection class RegionInfo(NamedTuple): game_scene: str # the name of the scene in the actual game dead_end: int = 0 # if a region has only one exit + outlet_region: Optional[str] = None + is_fake_region: bool = False + + +# gets the outlet region name if it exists, the region if it doesn't +def get_portal_outlet_region(portal: Portal, world: "TunicWorld") -> str: + return world.er_regions[portal.region].outlet_region or portal.region class DeadEnd(IntEnum): @@ -558,11 +570,11 @@ class DeadEnd(IntEnum): "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal "Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal "Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), # the small space between the door and the portal + "Overworld Fountain Cross Door": RegionInfo("Overworld Redux", outlet_region="Overworld"), "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Town Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal - "Overworld Spawn Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal - "Cube Cave Entrance Region": RegionInfo("Overworld Redux"), # other side of the bomb wall + "Overworld Town Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"), + "Overworld Spawn Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"), + "Cube Cave Entrance Region": RegionInfo("Overworld Redux", outlet_region="Overworld"), # other side of the bomb wall "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats), "Windmill": RegionInfo("Windmill"), "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door @@ -591,7 +603,7 @@ class DeadEnd(IntEnum): "Forest Belltower Lower": RegionInfo("Forest Belltower"), "East Forest": RegionInfo("East Forest Redux"), "East Forest Dance Fox Spot": RegionInfo("East Forest Redux"), - "East Forest Portal": RegionInfo("East Forest Redux"), + "East Forest Portal": RegionInfo("East Forest Redux", outlet_region="East Forest"), "Lower Forest": RegionInfo("East Forest Redux"), # bottom of the forest "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"), "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"), @@ -601,7 +613,7 @@ class DeadEnd(IntEnum): "Forest Grave Path Main": RegionInfo("Sword Access"), "Forest Grave Path Upper": RegionInfo("Sword Access"), "Forest Grave Path by Grave": RegionInfo("Sword Access"), - "Forest Hero's Grave": RegionInfo("Sword Access"), + "Forest Hero's Grave": RegionInfo("Sword Access", outlet_region="Forest Grave Path by Grave"), "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits "Dark Tomb Upper": RegionInfo("Crypt Redux"), # the part with the casket and the top of the ladder "Dark Tomb Main": RegionInfo("Crypt Redux"), @@ -614,18 +626,19 @@ class DeadEnd(IntEnum): "Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests "West Garden": RegionInfo("Archipelagos Redux"), "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats), - "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), + "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"), + "West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), "West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"), "West Garden after Boss": RegionInfo("Archipelagos Redux"), - "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux"), + "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden"), "Ruined Atoll": RegionInfo("Atoll Redux"), "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"), "Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"), "Ruined Atoll Frog Eye": RegionInfo("Atoll Redux"), - "Ruined Atoll Portal": RegionInfo("Atoll Redux"), - "Ruined Atoll Statue": RegionInfo("Atoll Redux"), + "Ruined Atoll Portal": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"), + "Ruined Atoll Statue": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"), "Frog Stairs Eye Exit": RegionInfo("Frog Stairs"), "Frog Stairs Upper": RegionInfo("Frog Stairs"), "Frog Stairs Lower": RegionInfo("Frog Stairs"), @@ -633,18 +646,20 @@ class DeadEnd(IntEnum): "Frog's Domain Entry": RegionInfo("frog cave main"), "Frog's Domain": RegionInfo("frog cave main"), "Frog's Domain Back": RegionInfo("frog cave main"), - "Library Exterior Tree Region": RegionInfo("Library Exterior"), + "Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"), + "Library Exterior by Tree": RegionInfo("Library Exterior"), "Library Exterior Ladder Region": RegionInfo("Library Exterior"), "Library Hall Bookshelf": RegionInfo("Library Hall"), "Library Hall": RegionInfo("Library Hall"), - "Library Hero's Grave Region": RegionInfo("Library Hall"), + "Library Hero's Grave Region": RegionInfo("Library Hall", outlet_region="Library Hall"), "Library Hall to Rotunda": RegionInfo("Library Hall"), "Library Rotunda to Hall": RegionInfo("Library Rotunda"), "Library Rotunda": RegionInfo("Library Rotunda"), "Library Rotunda to Lab": RegionInfo("Library Rotunda"), "Library Lab": RegionInfo("Library Lab"), "Library Lab Lower": RegionInfo("Library Lab"), - "Library Portal": RegionInfo("Library Lab"), + "Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"), + "Library Lab on Portal Pad": RegionInfo("Library Lab"), "Library Lab to Librarian": RegionInfo("Library Lab"), "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats), "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), @@ -663,22 +678,22 @@ class DeadEnd(IntEnum): "Fortress Grave Path": RegionInfo("Fortress Reliquary"), "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted), "Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"), - "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary"), + "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path"), "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats), "Fortress Arena": RegionInfo("Fortress Arena"), - "Fortress Arena Portal": RegionInfo("Fortress Arena"), + "Fortress Arena Portal": RegionInfo("Fortress Arena", outlet_region="Fortress Arena"), "Lower Mountain": RegionInfo("Mountain"), "Lower Mountain Stairs": RegionInfo("Mountain"), "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats), "Quarry Connector": RegionInfo("Darkwoods Tunnel"), "Quarry Entry": RegionInfo("Quarry Redux"), "Quarry": RegionInfo("Quarry Redux"), - "Quarry Portal": RegionInfo("Quarry Redux"), + "Quarry Portal": RegionInfo("Quarry Redux", outlet_region="Quarry Entry"), "Quarry Back": RegionInfo("Quarry Redux"), "Quarry Monastery Entry": RegionInfo("Quarry Redux"), "Monastery Front": RegionInfo("Monastery"), "Monastery Back": RegionInfo("Monastery"), - "Monastery Hero's Grave Region": RegionInfo("Monastery"), + "Monastery Hero's Grave Region": RegionInfo("Monastery", outlet_region="Monastery Back"), "Monastery Rope": RegionInfo("Quarry Redux"), "Lower Quarry": RegionInfo("Quarry Redux"), "Even Lower Quarry": RegionInfo("Quarry Redux"), @@ -691,19 +706,21 @@ class DeadEnd(IntEnum): "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"), "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side - "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special), # the exit from zig skip, for use with fixed shop on - "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side - "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"), + "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Front"), # the exit from zig skip, for use with fixed shop on + "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side + "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"), + "Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"), "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), "Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south "Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door "Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door - "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), # just the door + "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2", outlet_region="Swamp Ledge under Cathedral Door"), # just the door "Swamp to Cathedral Main Entrance Region": RegionInfo("Swamp Redux 2"), # just the door "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance - "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2"), + "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2", outlet_region="Back of Swamp"), "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse "Cathedral": RegionInfo("Cathedral Redux"), + "Cathedral to Gauntlet": RegionInfo("Cathedral Redux"), # the elevator "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats), "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), "Cathedral Gauntlet": RegionInfo("Cathedral Arena"), @@ -711,10 +728,10 @@ class DeadEnd(IntEnum): "Far Shore": RegionInfo("Transit"), "Far Shore to Spawn Region": RegionInfo("Transit"), "Far Shore to East Forest Region": RegionInfo("Transit"), - "Far Shore to Quarry Region": RegionInfo("Transit"), - "Far Shore to Fortress Region": RegionInfo("Transit"), - "Far Shore to Library Region": RegionInfo("Transit"), - "Far Shore to West Garden Region": RegionInfo("Transit"), + "Far Shore to Quarry Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to Fortress Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to Library Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to West Garden Region": RegionInfo("Transit", outlet_region="Far Shore"), "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), @@ -728,6 +745,16 @@ class DeadEnd(IntEnum): } +# this is essentially a pared down version of the region connections in rules.py, with some minor differences +# the main purpose of this is to make it so that you can access every region +# most items are excluded from the rules here, since we can assume Archipelago will properly place them +# laurels (hyperdash) can be locked at 10 fairies, requiring access to secret gathering place +# so until secret gathering place has been paired, you do not have hyperdash, so you cannot use hyperdash entrances +# Zip means you need the laurels zips option enabled +# IG# refers to ice grappling difficulties +# LS# refers to ladder storage difficulties +# LS rules are used for region connections here regardless of whether you have being knocked out of the air in logic +# this is because it just means you can reach the entrances in that region via ladder storage traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld": { "Overworld Beach": @@ -735,13 +762,13 @@ class DeadEnd(IntEnum): "Overworld to Atoll Upper": [["Hyperdash"]], "Overworld Belltower": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Swamp Upper Entry": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Swamp Lower Entry": [], "Overworld Special Shop Entry": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Well Ladder": [], "Overworld Ruined Passage Door": @@ -759,11 +786,11 @@ class DeadEnd(IntEnum): "Overworld after Envoy": [], "Overworld Quarry Entry": - [["NMG"]], + [["IG2"], ["LS1"]], "Overworld Tunnel Turret": - [["NMG"], ["Hyperdash"]], + [["IG1"], ["LS1"], ["Hyperdash"]], "Overworld Temple Door": - [["NMG"], ["Forest Belltower Upper", "Overworld Belltower"]], + [["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]], "Overworld Southeast Cross Door": [], "Overworld Fountain Cross Door": @@ -773,25 +800,28 @@ class DeadEnd(IntEnum): "Overworld Spawn Portal": [], "Overworld Well to Furnace Rail": - [["UR"]], + [["LS2"]], "Overworld Old House Door": [], "Cube Cave Entrance Region": [], + # drop a rudeling, icebolt or ice bomb + "Overworld to West Garden from Furnace": + [["IG3"]], }, "East Overworld": { "Above Ruined Passage": [], "After Ruined Passage": - [["NMG"]], - "Overworld": - [], + [["IG1"], ["LS1"]], + # "Overworld": + # [], "Overworld at Patrol Cave": [], "Overworld above Patrol Cave": [], "Overworld Special Shop Entry": - [["Hyperdash"], ["UR"]] + [["Hyperdash"], ["LS1"]] }, "Overworld Special Shop Entry": { "East Overworld": @@ -800,8 +830,8 @@ class DeadEnd(IntEnum): "Overworld Belltower": { "Overworld Belltower at Bell": [], - "Overworld": - [], + # "Overworld": + # [], "Overworld to West Garden Upper": [], }, @@ -809,19 +839,19 @@ class DeadEnd(IntEnum): "Overworld Belltower": [], }, - "Overworld Swamp Upper Entry": { - "Overworld": - [], - }, - "Overworld Swamp Lower Entry": { - "Overworld": - [], - }, + # "Overworld Swamp Upper Entry": { + # "Overworld": + # [], + # }, + # "Overworld Swamp Lower Entry": { + # "Overworld": + # [], + # }, "Overworld Beach": { - "Overworld": - [], + # "Overworld": + # [], "Overworld West Garden Laurels Entry": - [["Hyperdash"]], + [["Hyperdash"], ["LS1"]], "Overworld to Atoll Upper": [], "Overworld Tunnel Turret": @@ -832,38 +862,37 @@ class DeadEnd(IntEnum): [["Hyperdash"]], }, "Overworld to Atoll Upper": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, "Overworld Tunnel Turret": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, "Overworld Well Ladder": { - "Overworld": - [], + # "Overworld": + # [], }, "Overworld at Patrol Cave": { "East Overworld": - [["Hyperdash"]], + [["Hyperdash"], ["LS1"], ["IG1"]], "Overworld above Patrol Cave": [], }, "Overworld above Patrol Cave": { - "Overworld": - [], + # "Overworld": + # [], "East Overworld": [], "Upper Overworld": [], "Overworld at Patrol Cave": [], - "Overworld Belltower at Bell": - [["NMG"]], + # readd long dong if we ever do a misc tricks option }, "Upper Overworld": { "Overworld above Patrol Cave": @@ -878,51 +907,49 @@ class DeadEnd(IntEnum): [], }, "Overworld above Quarry Entrance": { - "Overworld": - [], + # "Overworld": + # [], "Upper Overworld": [], }, "Overworld Quarry Entry": { "Overworld after Envoy": [], - "Overworld": - [["NMG"]], + # "Overworld": + # [["IG1"]], }, "Overworld after Envoy": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Quarry Entry": [], }, "After Ruined Passage": { - "Overworld": - [], + # "Overworld": + # [], "Above Ruined Passage": [], - "East Overworld": - [["NMG"]], }, "Above Ruined Passage": { - "Overworld": - [], + # "Overworld": + # [], "After Ruined Passage": [], "East Overworld": [], }, - "Overworld Ruined Passage Door": { - "Overworld": - [["Hyperdash", "NMG"]], - }, - "Overworld Town Portal": { - "Overworld": - [], - }, - "Overworld Spawn Portal": { - "Overworld": - [], - }, + # "Overworld Ruined Passage Door": { + # "Overworld": + # [["Hyperdash", "Zip"]], + # }, + # "Overworld Town Portal": { + # "Overworld": + # [], + # }, + # "Overworld Spawn Portal": { + # "Overworld": + # [], + # }, "Cube Cave Entrance Region": { "Overworld": [], @@ -933,7 +960,7 @@ class DeadEnd(IntEnum): }, "Old House Back": { "Old House Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, "Furnace Fuse": { "Furnace Ladder Area": @@ -941,9 +968,9 @@ class DeadEnd(IntEnum): }, "Furnace Ladder Area": { "Furnace Fuse": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Furnace Walking Path": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, "Furnace Walking Path": { "Furnace Ladder Area": @@ -971,7 +998,7 @@ class DeadEnd(IntEnum): }, "East Forest": { "East Forest Dance Fox Spot": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"], ["LS1"]], "East Forest Portal": [], "Lower Forest": @@ -979,7 +1006,7 @@ class DeadEnd(IntEnum): }, "East Forest Dance Fox Spot": { "East Forest": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"]], }, "East Forest Portal": { "East Forest": @@ -995,7 +1022,7 @@ class DeadEnd(IntEnum): }, "Guard House 1 West": { "Guard House 1 East": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, "Guard House 2 Upper": { "Guard House 2 Lower": @@ -1007,19 +1034,19 @@ class DeadEnd(IntEnum): }, "Forest Grave Path Main": { "Forest Grave Path Upper": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"], ["IG3"]], "Forest Grave Path by Grave": [], }, "Forest Grave Path Upper": { "Forest Grave Path Main": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"]], }, "Forest Grave Path by Grave": { "Forest Hero's Grave": [], "Forest Grave Path Main": - [["NMG"]], + [["IG1"]], }, "Forest Hero's Grave": { "Forest Grave Path by Grave": @@ -1051,7 +1078,7 @@ class DeadEnd(IntEnum): }, "Dark Tomb Checkpoint": { "Well Boss": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, "Dark Tomb Entry Point": { "Dark Tomb Upper": @@ -1075,13 +1102,13 @@ class DeadEnd(IntEnum): }, "West Garden": { "West Garden Laurels Exit Region": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "West Garden after Boss": [], "West Garden Hero's Grave Region": [], "West Garden Portal Item": - [["NMG"]], + [["IG2"]], }, "West Garden Laurels Exit Region": { "West Garden": @@ -1093,13 +1120,19 @@ class DeadEnd(IntEnum): }, "West Garden Portal Item": { "West Garden": - [["NMG"]], - "West Garden Portal": - [["Hyperdash", "West Garden"]], + [["IG1"]], + "West Garden by Portal": + [["Hyperdash"]], }, - "West Garden Portal": { + "West Garden by Portal": { "West Garden Portal Item": [["Hyperdash"]], + "West Garden Portal": + [["West Garden"]], + }, + "West Garden Portal": { + "West Garden by Portal": + [], }, "West Garden Hero's Grave Region": { "West Garden": @@ -1107,7 +1140,7 @@ class DeadEnd(IntEnum): }, "Ruined Atoll": { "Ruined Atoll Lower Entry Area": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Ruined Atoll Ladder Tops": [], "Ruined Atoll Frog Mouth": @@ -1174,11 +1207,17 @@ class DeadEnd(IntEnum): [], }, "Library Exterior Ladder Region": { + "Library Exterior by Tree": + [], + }, + "Library Exterior by Tree": { "Library Exterior Tree Region": [], + "Library Exterior Ladder Region": + [], }, "Library Exterior Tree Region": { - "Library Exterior Ladder Region": + "Library Exterior by Tree": [], }, "Library Hall Bookshelf": { @@ -1223,15 +1262,21 @@ class DeadEnd(IntEnum): "Library Lab": { "Library Lab Lower": [["Hyperdash"]], - "Library Portal": + "Library Lab on Portal Pad": [], "Library Lab to Librarian": [], }, - "Library Portal": { + "Library Lab on Portal Pad": { + "Library Portal": + [], "Library Lab": [], }, + "Library Portal": { + "Library Lab on Portal Pad": + [], + }, "Library Lab to Librarian": { "Library Lab": [], @@ -1240,11 +1285,9 @@ class DeadEnd(IntEnum): "Fortress Exterior from Overworld": [], "Fortress Courtyard Upper": - [["UR"]], - "Fortress Exterior near cave": - [["UR"]], + [["LS2"]], "Fortress Courtyard": - [["UR"]], + [["LS1"]], }, "Fortress Exterior from Overworld": { "Fortress Exterior from East Forest": @@ -1252,15 +1295,15 @@ class DeadEnd(IntEnum): "Fortress Exterior near cave": [], "Fortress Courtyard": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"], ["LS1"]], }, "Fortress Exterior near cave": { "Fortress Exterior from Overworld": - [["Hyperdash"], ["UR"]], - "Fortress Courtyard": - [["UR"]], + [["Hyperdash"], ["LS1"]], + "Fortress Courtyard": # ice grapple hard: shoot far fire pot, it aggros one of the enemies over to you + [["IG3"], ["LS1"]], "Fortress Courtyard Upper": - [["UR"]], + [["LS2"]], "Beneath the Vault Entry": [], }, @@ -1270,7 +1313,7 @@ class DeadEnd(IntEnum): }, "Fortress Courtyard": { "Fortress Courtyard Upper": - [["NMG"]], + [["IG1"]], "Fortress Exterior from Overworld": [["Hyperdash"]], }, @@ -1296,7 +1339,7 @@ class DeadEnd(IntEnum): }, "Fortress East Shortcut Lower": { "Fortress East Shortcut Upper": - [["NMG"]], + [["IG1"]], }, "Fortress East Shortcut Upper": { "Fortress East Shortcut Lower": @@ -1304,11 +1347,11 @@ class DeadEnd(IntEnum): }, "Eastern Vault Fortress": { "Eastern Vault Fortress Gold Door": - [["NMG"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], + [["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], }, "Eastern Vault Fortress Gold Door": { "Eastern Vault Fortress": - [["NMG"]], + [["IG1"]], }, "Fortress Grave Path": { "Fortress Hero's Grave Region": @@ -1318,7 +1361,7 @@ class DeadEnd(IntEnum): }, "Fortress Grave Path Upper": { "Fortress Grave Path": - [["NMG"]], + [["IG1"]], }, "Fortress Grave Path Dusty Entrance Region": { "Fortress Grave Path": @@ -1346,7 +1389,7 @@ class DeadEnd(IntEnum): }, "Monastery Back": { "Monastery Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], "Monastery Hero's Grave Region": [], }, @@ -1363,6 +1406,8 @@ class DeadEnd(IntEnum): [["Quarry Connector"]], "Quarry": [], + "Monastery Rope": + [["LS2"]], }, "Quarry Portal": { "Quarry Entry": @@ -1374,7 +1419,7 @@ class DeadEnd(IntEnum): "Quarry Back": [["Hyperdash"]], "Monastery Rope": - [["UR"]], + [["LS2"]], }, "Quarry Back": { "Quarry": @@ -1392,7 +1437,7 @@ class DeadEnd(IntEnum): "Quarry Monastery Entry": [], "Lower Quarry Zig Door": - [["NMG"]], + [["IG3"]], }, "Lower Quarry": { "Even Lower Quarry": @@ -1402,7 +1447,7 @@ class DeadEnd(IntEnum): "Lower Quarry": [], "Lower Quarry Zig Door": - [["Quarry", "Quarry Connector"], ["NMG"]], + [["Quarry", "Quarry Connector"], ["IG3"]], }, "Monastery Rope": { "Quarry Back": @@ -1430,7 +1475,7 @@ class DeadEnd(IntEnum): }, "Rooted Ziggurat Lower Back": { "Rooted Ziggurat Lower Front": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"], ["IG1"]], "Rooted Ziggurat Portal Room Entrance": [], }, @@ -1443,26 +1488,35 @@ class DeadEnd(IntEnum): [], }, "Rooted Ziggurat Portal Room Exit": { - "Rooted Ziggurat Portal": + "Rooted Ziggurat Portal Room": [], }, - "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room": { + "Rooted Ziggurat Portal": + [], "Rooted Ziggurat Portal Room Exit": [["Rooted Ziggurat Lower Back"]], }, + "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room": + [], + }, "Swamp Front": { "Swamp Mid": [], + # get one pillar from the gate, then dash onto the gate, very tricky + "Back of Swamp Laurels Area": + [["Hyperdash", "Zip"]], }, "Swamp Mid": { "Swamp Front": [], "Swamp to Cathedral Main Entrance Region": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG2"], ["LS3"]], "Swamp Ledge under Cathedral Door": [], "Back of Swamp": - [["UR"]], + [["LS1"]], # ig3 later? }, "Swamp Ledge under Cathedral Door": { "Swamp Mid": @@ -1476,24 +1530,41 @@ class DeadEnd(IntEnum): }, "Swamp to Cathedral Main Entrance Region": { "Swamp Mid": - [["NMG"]], + [["IG1"]], }, "Back of Swamp": { "Back of Swamp Laurels Area": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"]], "Swamp Hero's Grave Region": [], + "Swamp Mid": + [["LS2"]], + "Swamp Front": + [["LS1"]], + "Swamp to Cathedral Main Entrance Region": + [["LS3"]], + "Swamp to Cathedral Treasure Room": + [["LS3"]] }, "Back of Swamp Laurels Area": { "Back of Swamp": [["Hyperdash"]], + # get one pillar from the gate, then dash onto the gate, very tricky "Swamp Mid": - [["NMG", "Hyperdash"]], + [["IG1", "Hyperdash"], ["Hyperdash", "Zip"]], }, "Swamp Hero's Grave Region": { "Back of Swamp": [], }, + "Cathedral": { + "Cathedral to Gauntlet": + [], + }, + "Cathedral to Gauntlet": { + "Cathedral": + [], + }, "Cathedral Gauntlet Checkpoint": { "Cathedral Gauntlet": [], diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 3d1973beb375..bd2498a56a35 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1,8 +1,10 @@ -from typing import Dict, Set, List, Tuple, TYPE_CHECKING +from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING from worlds.generic.Rules import set_rule, forbid_item +from .options import IceGrappling, LadderStorage from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, - bomb_walls) -from .er_data import Portal + laurels_zip, bomb_walls) +from .er_data import Portal, get_portal_outlet_region +from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls from BaseClasses import Region, CollectionState if TYPE_CHECKING: @@ -82,13 +84,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Belltower"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld"]) + # ice grapple rudeling across rubble, drop bridge, ice grapple rudeling down regions["Overworld Belltower"].connect( connecting_region=regions["Overworld to West Garden Upper"], - rule=lambda state: has_ladder("Ladders to West Bell", state, world)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld to West Garden Upper"].connect( connecting_region=regions["Overworld Belltower"], rule=lambda state: has_ladder("Ladders to West Bell", state, world)) @@ -97,32 +102,35 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld Belltower at Bell"], rule=lambda state: has_ladder("Ladders to West Bell", state, world)) - # long dong, do not make a reverse connection here or to belltower - regions["Overworld above Patrol Cave"].connect( - connecting_region=regions["Overworld Belltower at Bell"], - rule=lambda state: options.logic_rules and state.has(fire_wand, player)) + # long dong, do not make a reverse connection here or to belltower, maybe readd later + # regions["Overworld above Patrol Cave"].connect( + # connecting_region=regions["Overworld Belltower at Bell"], + # rule=lambda state: options.logic_rules and state.has(fire_wand, player)) - # nmg: can laurels through the ruined passage door + # can laurels through the ruined passage door at either corner regions["Overworld"].connect( connecting_region=regions["Overworld Ruined Passage Door"], rule=lambda state: state.has(key, player, 2) - or (state.has(laurels, player) and options.logic_rules)) + or laurels_zip(state, world)) regions["Overworld Ruined Passage Door"].connect( connecting_region=regions["Overworld"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Overworld"].connect( connecting_region=regions["After Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) + # for the hard ice grapple, get to the chest after the bomb wall, grab a slime, and grapple push down + # you can ice grapple through the bomb wall, so no need for shop logic checking regions["Overworld"].connect( connecting_region=regions["Above Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or state.has(laurels, player)) + or state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) @@ -138,7 +146,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Above Ruined Passage"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Overworld"].connect( connecting_region=regions["Above Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) @@ -147,15 +155,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # nmg: ice grapple the slimes, works both ways consistently regions["East Overworld"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) @@ -169,7 +177,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld at Patrol Cave"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Overworld at Patrol Cave"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)) @@ -185,7 +193,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["East Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) @@ -193,7 +201,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Upper Overworld"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Upper Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) @@ -206,13 +214,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Upper Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) + # ice grapple push guard captain down the ledge regions["Upper Overworld"].connect( connecting_region=regions["Overworld after Temple Rafters"], - rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) regions["Overworld after Temple Rafters"].connect( connecting_region=regions["Upper Overworld"], rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Quarry Entrance"].connect( connecting_region=regions["Overworld"], @@ -224,13 +234,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], rule=lambda state: state.has_any({laurels, grapple, gun}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({laurels, grapple, gun}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld Quarry Entry"], @@ -242,10 +250,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # ice grapple through the gate regions["Overworld"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], @@ -256,7 +264,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Lower Entry"], - rule=lambda state: has_ladder("Ladder to Swamp", state, world)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld Swamp Lower Entry"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladder to Swamp", state, world)) @@ -279,20 +288,21 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Old House Door"], rule=lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - # not including ice grapple through this because it's very tedious to get an enemy here + # lure enemy over and ice grapple through regions["Overworld"].connect( connecting_region=regions["Overworld Southeast Cross Door"], - rule=lambda state: has_ability(holy_cross, state, world)) + rule=lambda state: has_ability(holy_cross, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld Southeast Cross Door"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ability(holy_cross, state, world)) - # not including ice grapple through this because we're not including it on the other door regions["Overworld"].connect( connecting_region=regions["Overworld Fountain Cross Door"], - rule=lambda state: has_ability(holy_cross, state, world)) + rule=lambda state: has_ability(holy_cross, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Fountain Cross Door"].connect( connecting_region=regions["Overworld"]) @@ -312,7 +322,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Temple Door"], rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player) - or has_ice_grapple_logic(False, state, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Temple Door"].connect( connecting_region=regions["Overworld above Patrol Cave"], @@ -325,12 +335,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld Beach"].connect( connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Tunnel Turret"], - rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, world)) + rule=lambda state: state.has(laurels, player)) regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) @@ -341,13 +350,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Cube Cave Entrance Region"].connect( connecting_region=regions["Overworld"]) + # drop a rudeling down, icebolt or ice bomb + regions["Overworld"].connect( + connecting_region=regions["Overworld to West Garden from Furnace"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) + # Overworld side areas regions["Old House Front"].connect( connecting_region=regions["Old House Back"]) - # nmg: laurels through the gate + # laurels through the gate, use left wall to space yourself regions["Old House Back"].connect( connecting_region=regions["Old House Front"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Sealed Temple"].connect( connecting_region=regions["Sealed Temple Rafters"]) @@ -388,15 +402,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Forest Belltower Lower"], rule=lambda state: has_ladder("Ladder to East Forest", state, world)) - # nmg: ice grapple up to dance fox spot, and vice versa + # ice grapple up to dance fox spot, and vice versa regions["East Forest"].connect( connecting_region=regions["East Forest Dance Fox Spot"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Forest Dance Fox Spot"].connect( connecting_region=regions["East Forest"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Forest"].connect( connecting_region=regions["East Forest Portal"], @@ -407,7 +421,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["East Forest"].connect( connecting_region=regions["Lower Forest"], rule=lambda state: has_ladder("Ladders to Lower Forest", state, world) - or (state.has_all({grapple, fire_wand, ice_dagger}, player) and has_ability(icebolt, state, world))) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Lower Forest"].connect( connecting_region=regions["East Forest"], rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) @@ -425,22 +439,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Guard House 2 Upper"], rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) - # nmg: ice grapple from upper grave path exit to the rest of it + # ice grapple from upper grave path exit to the rest of it regions["Forest Grave Path Upper"].connect( connecting_region=regions["Forest Grave Path Main"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + # for the ice grapple, lure a rudeling up top, then grapple push it across regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path Upper"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path by Grave"]) - # nmg: ice grapple or laurels through the gate + # ice grapple or laurels through the gate regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Grave Path Main"], - rule=lambda state: has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + or laurels_zip(state, world)) regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Hero's Grave"], @@ -473,10 +489,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Well Boss"].connect( connecting_region=regions["Dark Tomb Checkpoint"]) - # nmg: can laurels through the gate + # can laurels through the gate, no setup needed regions["Dark Tomb Checkpoint"].connect( connecting_region=regions["Well Boss"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Dark Tomb Entry Point"].connect( connecting_region=regions["Dark Tomb Upper"], @@ -505,12 +521,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["West Garden Laurels Exit Region"], rule=lambda state: state.has(laurels, player)) + # you can grapple Garden Knight to aggro it, then ledge it regions["West Garden after Boss"].connect( connecting_region=regions["West Garden"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + # ice grapple push Garden Knight off the side regions["West Garden"].connect( connecting_region=regions["West Garden after Boss"], - rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + rule=lambda state: state.has(laurels, player) or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Hero's Grave Region"], @@ -519,26 +539,32 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["West Garden"]) regions["West Garden Portal"].connect( + connecting_region=regions["West Garden by Portal"]) + regions["West Garden by Portal"].connect( + connecting_region=regions["West Garden Portal"], + rule=lambda state: has_ability(prayer, state, world) and state.has("Activate West Garden Fuse", player)) + + regions["West Garden by Portal"].connect( connecting_region=regions["West Garden Portal Item"], rule=lambda state: state.has(laurels, player)) regions["West Garden Portal Item"].connect( - connecting_region=regions["West Garden Portal"], - rule=lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + connecting_region=regions["West Garden by Portal"], + rule=lambda state: state.has(laurels, player)) - # nmg: can ice grapple to and from the item behind the magic dagger house + # can ice grapple to and from the item behind the magic dagger house regions["West Garden Portal Item"].connect( connecting_region=regions["West Garden"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Portal Item"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) # Atoll and Frog's Domain - # nmg: ice grapple the bird below the portal + # ice grapple the bird below the portal regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Lower Entry Area"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Ruined Atoll Lower Entry Area"].connect( connecting_region=regions["Ruined Atoll"], rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) @@ -570,13 +596,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], rule=lambda state: has_ability(prayer, state, world) - and has_ladder("Ladders in South Atoll", state, world)) + and (has_ladder("Ladders in South Atoll", state, world) + # shoot fuse and have the shot hit you mid-LS + or (can_ladder_storage(state, world) and state.has(fire_wand, player) + and options.ladder_storage >= LadderStorage.option_hard))) regions["Ruined Atoll Statue"].connect( connecting_region=regions["Ruined Atoll"]) regions["Frog Stairs Eye Exit"].connect( connecting_region=regions["Frog Stairs Upper"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Frog Stairs Upper"].connect( connecting_region=regions["Frog Stairs Eye Exit"], rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) @@ -605,14 +635,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # Library regions["Library Exterior Tree Region"].connect( + connecting_region=regions["Library Exterior by Tree"]) + regions["Library Exterior by Tree"].connect( + connecting_region=regions["Library Exterior Tree Region"], + rule=lambda state: has_ability(prayer, state, world)) + + regions["Library Exterior by Tree"].connect( connecting_region=regions["Library Exterior Ladder Region"], rule=lambda state: state.has_any({grapple, laurels}, player) and has_ladder("Ladders in Library", state, world)) regions["Library Exterior Ladder Region"].connect( - connecting_region=regions["Library Exterior Tree Region"], - rule=lambda state: has_ability(prayer, state, world) - and ((state.has(laurels, player) and has_ladder("Ladders in Library", state, world)) - or state.has(grapple, player))) + connecting_region=regions["Library Exterior by Tree"], + rule=lambda state: state.has(grapple, player) + or (state.has(laurels, player) and has_ladder("Ladders in Library", state, world))) regions["Library Hall Bookshelf"].connect( connecting_region=regions["Library Hall"], @@ -658,14 +693,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( - connecting_region=regions["Library Portal"], - rule=lambda state: has_ability(prayer, state, world) - and has_ladder("Ladders in Library", state, world)) - regions["Library Portal"].connect( + connecting_region=regions["Library Lab on Portal Pad"], + rule=lambda state: has_ladder("Ladders in Library", state, world)) + regions["Library Lab on Portal Pad"].connect( connecting_region=regions["Library Lab"], rule=lambda state: has_ladder("Ladders in Library", state, world) or state.has(laurels, player)) + regions["Library Lab on Portal Pad"].connect( + connecting_region=regions["Library Portal"], + rule=lambda state: has_ability(prayer, state, world)) + regions["Library Portal"].connect( + connecting_region=regions["Library Lab on Portal Pad"]) + regions["Library Lab"].connect( connecting_region=regions["Library Lab to Librarian"], rule=lambda state: has_ladder("Ladders in Library", state, world)) @@ -688,6 +728,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Fortress Exterior near cave"], rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world)) + # shoot far fire pot, enemy gets aggro'd + regions["Fortress Exterior near cave"].connect( + connecting_region=regions["Fortress Courtyard"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) + regions["Fortress Exterior near cave"].connect( connecting_region=regions["Beneath the Vault Entry"], rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) @@ -702,14 +747,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Courtyard"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Courtyard"]) # nmg: can ice grapple to the upper ledge regions["Fortress Courtyard"].connect( connecting_region=regions["Fortress Courtyard Upper"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Exterior from Overworld"]) @@ -717,7 +762,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Beneath the Vault Ladder Exit"].connect( connecting_region=regions["Beneath the Vault Main"], rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) - and has_lantern(state, world)) + and has_lantern(state, world) + # there's some boxes in the way + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player))) + # on the reverse trip, you can lure an enemy over to break the boxes if needed regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) @@ -733,17 +781,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # nmg: can ice grapple upwards regions["Fortress East Shortcut Lower"].connect( connecting_region=regions["Fortress East Shortcut Upper"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) # nmg: ice grapple through the big gold door, can do it both ways regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", "Activate Eastern Vault East Fuse"}, player) - or has_ice_grapple_logic(False, state, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Eastern Vault Fortress Gold Door"].connect( connecting_region=regions["Eastern Vault Fortress"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Fortress Grave Path"].connect( connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], @@ -761,7 +809,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # nmg: ice grapple from upper grave path to lower regions["Fortress Grave Path Upper"].connect( connecting_region=regions["Fortress Grave Path"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], @@ -819,25 +867,25 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Lower Quarry"].connect( connecting_region=regions["Even Lower Quarry"], rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) # nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock regions["Even Lower Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], rule=lambda state: state.has("Activate Quarry Fuse", player) - or (has_ice_grapple_logic(False, state, world) and options.entrance_rando)) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on regions["Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], - rule=lambda state: has_ice_grapple_logic(True, state, world) and options.entrance_rando) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) regions["Monastery Front"].connect( connecting_region=regions["Monastery Back"]) - # nmg: can laurels through the gate + # laurels through the gate, no setup needed regions["Monastery Back"].connect( connecting_region=regions["Monastery Front"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Monastery Back"].connect( connecting_region=regions["Monastery Hero's Grave Region"], @@ -863,14 +911,13 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Back"], rule=lambda state: state.has(laurels, player) or (has_sword(state, player) and has_ability(prayer, state, world))) - # unrestricted: use ladder storage to get to the front, get hit by one of the many enemies # nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Lower Front"], - rule=lambda state: ((state.has(laurels, player) or has_ice_grapple_logic(True, state, world)) - and has_ability(prayer, state, world) - and has_sword(state, player)) - or can_ladder_storage(state, world)) + rule=lambda state: (state.has(laurels, player) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + and has_ability(prayer, state, world) + and has_sword(state, player)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], @@ -882,40 +929,62 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Front"]) regions["Rooted Ziggurat Portal"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room"]) + regions["Rooted Ziggurat Portal Room"].connect( + connecting_region=regions["Rooted Ziggurat Portal"], + rule=lambda state: has_ability(prayer, state, world)) + + regions["Rooted Ziggurat Portal Room"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Exit"], rule=lambda state: state.has("Activate Ziggurat Fuse", player)) regions["Rooted Ziggurat Portal Room Exit"].connect( - connecting_region=regions["Rooted Ziggurat Portal"], - rule=lambda state: has_ability(prayer, state, world)) + connecting_region=regions["Rooted Ziggurat Portal Room"]) # Swamp and Cathedral regions["Swamp Front"].connect( connecting_region=regions["Swamp Mid"], rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Swamp Mid"].connect( connecting_region=regions["Swamp Front"], rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - # nmg: ice grapple through cathedral door, can do it both ways - regions["Swamp Mid"].connect( + # a whole lot of stuff to basically say "you need to pray at the overworld fuse" + swamp_mid_to_cath = regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], - rule=lambda state: (has_ability(prayer, state, world) and state.has(laurels, player)) - or has_ice_grapple_logic(False, state, world)) + rule=lambda state: (has_ability(prayer, state, world) + and (state.has(laurels, player) + # blam yourself in the face with a wand shot off the fuse + or (can_ladder_storage(state, world) and state.has(fire_wand, player) + and options.ladder_storage >= LadderStorage.option_hard + and (not options.shuffle_ladders + or state.has_any({"Ladders in Overworld Town", + "Ladder to Swamp", + "Ladders near Weathervane"}, player) + or (state.has("Ladder to Ruined Atoll", player) + and state.can_reach_region("Overworld Beach", player)))))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + + if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders: + world.multiworld.register_indirect_condition(regions["Overworld Beach"], swamp_mid_to_cath) + regions["Swamp to Cathedral Main Entrance Region"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) + # grapple push the enemy by the door down, then grapple to it. Really jank regions["Swamp Mid"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"], - rule=lambda state: has_ladder("Ladders in Swamp", state, world)) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) + # ice grapple enemy standing at the door regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp Mid"], rule=lambda state: has_ladder("Ladders in Swamp", state, world) - or has_ice_grapple_logic(True, state, world)) # nmg: ice grapple the enemy at door + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp to Cathedral Treasure Room"], @@ -930,11 +999,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Back of Swamp"], rule=lambda state: state.has(laurels, player)) - # nmg: can ice grapple down while you're on the pillars + # ice grapple down from the pillar, or do that really annoying laurels zip + # the zip goes to front or mid, just doing mid since mid -> front can be done with laurels alone regions["Back of Swamp Laurels Area"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: state.has(laurels, player) - and has_ice_grapple_logic(True, state, world)) + rule=lambda state: laurels_zip(state, world) + or (state.has(laurels, player) + and has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))) + # get one pillar from the gate, then dash onto the gate, very tricky + regions["Swamp Front"].connect( + connecting_region=regions["Back of Swamp Laurels Area"], + rule=lambda state: laurels_zip(state, world)) regions["Back of Swamp"].connect( connecting_region=regions["Swamp Hero's Grave Region"], @@ -942,6 +1017,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) + regions["Cathedral"].connect( + connecting_region=regions["Cathedral to Gauntlet"], + rule=lambda state: (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + or options.entrance_rando) # elevator is always there in ER + regions["Cathedral to Gauntlet"].connect( + connecting_region=regions["Cathedral"]) + regions["Cathedral Gauntlet Checkpoint"].connect( connecting_region=regions["Cathedral Gauntlet"]) @@ -1000,337 +1083,141 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ and state.has_group_unique("Hero Relics", player, 6) and has_sword(state, player)))) - # connecting the regions portals are in to other portals you can access via ladder storage - # using has_stick instead of can_ladder_storage since it's already checking the logic rules - if options.logic_rules == "unrestricted": + if options.ladder_storage: def get_portal_info(portal_sd: str) -> Tuple[str, str]: for portal1, portal2 in portal_pairs.items(): if portal1.scene_destination() == portal_sd: - return portal1.name, portal2.region + return portal1.name, get_portal_outlet_region(portal2, world) if portal2.scene_destination() == portal_sd: - return portal2.name, portal1.region + return portal2.name, get_portal_outlet_region(portal1, world) raise Exception("no matches found in get_paired_region") - ladder_storages: List[Tuple[str, str, Set[str]]] = [ - # LS from Overworld main - # The upper Swamp entrance - ("Overworld", "Overworld Redux, Swamp Redux 2_wall", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Upper atoll entrance - ("Overworld", "Overworld Redux, Atoll Redux_upper", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Furnace entrance, next to the sign that leads to West Garden - ("Overworld", "Overworld Redux, Furnace_gyro_west", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Upper West Garden entry, by the belltower - ("Overworld", "Overworld Redux, Archipelagos Redux_upper", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Ruined Passage - ("Overworld", "Overworld Redux, Ruins Passage_east", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld", "Overworld Redux, Sewer_west_aqueduct", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), - # Quarry entry - ("Overworld", "Overworld Redux, Darkwoods Tunnel_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well"}), - # East Forest entry - ("Overworld", "Overworld Redux, Forest Belltower_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Fortress entry - ("Overworld", "Overworld Redux, Fortress Courtyard_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Patrol Cave entry - ("Overworld", "Overworld Redux, PatrolCave_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Special Shop entry, excluded in non-ER due to soft lock potential - ("Overworld", "Overworld Redux, ShopSpecial_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", - "Ladders near Dark Tomb"}), - # Temple Rafters, excluded in non-ER + ladder rando due to soft lock potential - ("Overworld", "Overworld Redux, Temple_rafters", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", - "Ladders near Dark Tomb"}), - # Spot above the Quarry entrance, - # only gets you to the mountain stairs - ("Overworld above Quarry Entrance", "Overworld Redux, Mountain_", - {"Ladders near Dark Tomb"}), - - # LS from the Overworld Beach - # West Garden entry by the Furnace - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lower", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # West Garden laurels entry - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lowest", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Swamp lower entrance - ("Overworld Beach", "Overworld Redux, Swamp Redux 2_conduit", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Rotating Lights entrance - ("Overworld Beach", "Overworld Redux, Overworld Cave_", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Swamp upper entrance - ("Overworld Beach", "Overworld Redux, Swamp Redux 2_wall", - {"Ladder to Ruined Atoll"}), - # Furnace entrance, next to the sign that leads to West Garden - ("Overworld Beach", "Overworld Redux, Furnace_gyro_west", - {"Ladder to Ruined Atoll"}), - # Upper West Garden entry, by the belltower - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_upper", - {"Ladder to Ruined Atoll"}), - # Ruined Passage - ("Overworld Beach", "Overworld Redux, Ruins Passage_east", - {"Ladder to Ruined Atoll"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld Beach", "Overworld Redux, Sewer_west_aqueduct", - {"Ladder to Ruined Atoll"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld Beach", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladder to Ruined Atoll"}), - # Quarry entry - ("Overworld Beach", "Overworld Redux, Darkwoods Tunnel_", - {"Ladder to Ruined Atoll"}), - - # LS from that low spot where you normally walk to swamp - # Only has low ones you can't get to from main Overworld - # West Garden main entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lower", - {"Ladder to Swamp"}), - # Maze Cave entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Maze Room_", - {"Ladder to Swamp"}), - # Hourglass Cave entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Town Basement_beach", - {"Ladder to Swamp"}), - # Lower Atoll entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Atoll Redux_lower", - {"Ladder to Swamp"}), - # Lowest West Garden entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lowest", - {"Ladder to Swamp"}), - - # from the ladders by the belltower - # Ruined Passage - ("Overworld to West Garden Upper", "Overworld Redux, Ruins Passage_east", - {"Ladders to West Bell"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld to West Garden Upper", "Overworld Redux, Sewer_west_aqueduct", - {"Ladders to West Bell"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld to West Garden Upper", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladders to West Bell"}), - # Quarry entry - ("Overworld to West Garden Upper", "Overworld Redux, Darkwoods Tunnel_", - {"Ladders to West Bell"}), - # East Forest entry - ("Overworld to West Garden Upper", "Overworld Redux, Forest Belltower_", - {"Ladders to West Bell"}), - # Fortress entry - ("Overworld to West Garden Upper", "Overworld Redux, Fortress Courtyard_", - {"Ladders to West Bell"}), - # Patrol Cave entry - ("Overworld to West Garden Upper", "Overworld Redux, PatrolCave_", - {"Ladders to West Bell"}), - # Special Shop entry, excluded in non-ER due to soft lock potential - ("Overworld to West Garden Upper", "Overworld Redux, ShopSpecial_", - {"Ladders to West Bell"}), - # Temple Rafters, excluded in non-ER and ladder rando due to soft lock potential - ("Overworld to West Garden Upper", "Overworld Redux, Temple_rafters", - {"Ladders to West Bell"}), - - # In the furnace - # Furnace ladder to the fuse entrance - ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north", set()), - # Furnace ladder to Dark Tomb - ("Furnace Ladder Area", "Furnace, Crypt Redux_", set()), - # Furnace ladder to the West Garden connector - ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west", set()), - - # West Garden - # exit after Garden Knight - ("West Garden", "Archipelagos Redux, Overworld Redux_upper", set()), - # West Garden laurels exit - ("West Garden", "Archipelagos Redux, Overworld Redux_lowest", set()), - - # Atoll, use the little ladder you fix at the beginning - ("Ruined Atoll", "Atoll Redux, Overworld Redux_lower", set()), - ("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth", set()), - ("Ruined Atoll", "Atoll Redux, Frog Stairs_eye", set()), - - # East Forest - # Entrance by the dancing fox holy cross spot - ("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper", set()), - - # From the west side of Guard House 1 to the east side - ("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate", set()), - ("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_", set()), - - # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch - ("Forest Grave Path Main", "Sword Access, East Forest Redux_upper", set()), - - # Fortress Exterior - # shop, ls at the ladder by the telescope - ("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_", set()), - # Fortress main entry and grave path lower entry, ls at the ladder by the telescope - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door", set()), - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower", set()), - # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper", set()), - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_", set()), - - # same as above, except from the east side of the area - ("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_", set()), - - # same as above, except from the Beneath the Vault entrance ladder - ("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", - {"Ladder to Beneath the Vault"}), - - # ls at the ladder, need to gain a little height to get up the stairs - # excluded in non-ER due to soft lock potential - ("Lower Mountain", "Mountain, Mountaintop_", set()), - - # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword - ("Quarry Monastery Entry", "Quarry Redux, Monastery_back", set()), - - # Swamp to Gauntlet - ("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", - {"Ladders in Swamp"}), - # Swamp to Overworld upper - ("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", - {"Ladders in Swamp"}), - # Ladder by the hero grave - ("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit", set()), - ("Back of Swamp", "Swamp Redux 2, Shop_", set()), - # Need to put the cathedral HC code mid-flight - ("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret", set()), - ] - - for region_name, scene_dest, ladders in ladder_storages: - portal_name, paired_region = get_portal_info(scene_dest) - # this is the only exception, requiring holy cross as well - if portal_name == "Swamp to Cathedral Secret Legend Room Entrance" and region_name == "Back of Swamp": - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and has_ability(holy_cross, state, world) - and (has_ladder("Ladders in Swamp", state, world) - or has_ice_grapple_logic(True, state, world) - or not options.entrance_rando)) - # soft locked without this ladder - elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and (state.has("Ladders to West Bell", player))) - # soft locked unless you have either ladder. if you have laurels, you use the other Entrance - elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \ - and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) - # soft locked for the same reasons as above - elif portal_name in {"Entrance to Furnace near West Garden", "West Garden Entrance from Furnace"} \ - and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) - and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) - # soft locked if you can't get past garden knight backwards or up the belltower ladders - elif portal_name == "West Garden Entrance near Belltower" and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) - and state.has_any({"Ladders to West Bell", laurels}, player)) - # soft locked if you can't get back out - elif portal_name == "Fortress Courtyard to Beneath the Vault" and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) and state.has("Ladder to Beneath the Vault", player) - and has_lantern(state, world)) - elif portal_name == "Atoll Lower Entrance" and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) - and (state.has_any({"Ladders in Overworld Town", grapple}, player) - or has_ice_grapple_logic(True, state, world))) - elif portal_name == "Atoll Upper Entrance" and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) - and state.has(grapple, player) or has_ability(prayer, state, world)) - # soft lock potential - elif portal_name in {"Special Shop Entrance", "Stairs to Top of the Mountain", "Swamp Upper Entrance", - "Swamp Lower Entrance", "Caustic Light Cave Entrance"} and not options.entrance_rando: + # connect ls elevation regions to their destinations + def ls_connect(origin_name: str, portal_sdt: str) -> None: + p_name, paired_region_name = get_portal_info(portal_sdt) + ladder_regions[origin_name].connect( + regions[paired_region_name], + name=p_name + " (LS) " + origin_name) + + # get what non-overworld ladder storage connections we want together + non_ow_ls_list = [] + non_ow_ls_list.extend(easy_ls) + if options.ladder_storage >= LadderStorage.option_medium: + non_ow_ls_list.extend(medium_ls) + if options.ladder_storage >= LadderStorage.option_hard: + non_ow_ls_list.extend(hard_ls) + + # create the ls elevation regions + ladder_regions: Dict[str, Region] = {} + for name in ow_ladder_groups.keys(): + ladder_regions[name] = Region(name, player, world.multiworld) + + # connect the ls elevations to each other where applicable + if options.ladder_storage >= LadderStorage.option_medium: + for i in range(len(ow_ladder_groups) - 1): + ladder_regions[f"LS Elev {i}"].connect(ladder_regions[f"LS Elev {i + 1}"]) + + # connect the applicable overworld regions to the ls elevation regions + for origin_region, ladders in region_ladders.items(): + for ladder_region, region_info in ow_ladder_groups.items(): + # checking if that region has a ladder or ladders for that elevation + common_ladders: FrozenSet[str] = frozenset(ladders.intersection(region_info.ladders)) + if common_ladders: + if options.shuffle_ladders: + regions[origin_region].connect( + connecting_region=ladder_regions[ladder_region], + rule=lambda state, lads=common_ladders: state.has_any(lads, player) + and can_ladder_storage(state, world)) + else: + regions[origin_region].connect( + connecting_region=ladder_regions[ladder_region], + rule=lambda state: can_ladder_storage(state, world)) + + # connect ls elevation regions to the region on the other side of the portals + for ladder_region, region_info in ow_ladder_groups.items(): + for portal_dest in region_info.portals: + ls_connect(ladder_region, "Overworld Redux, " + portal_dest) + + # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail + if options.ladder_storage >= LadderStorage.option_medium: + for ladder_region, region_info in ow_ladder_groups.items(): + for dest_region in region_info.regions: + ladder_regions[ladder_region].connect( + connecting_region=regions[dest_region], + name=ladder_region + " (LS) " + dest_region) + # well rail, need height off portal pad for one side, and a tiny extra from stairs on the other + ls_connect("LS Elev 3", "Overworld Redux, Sewer_west_aqueduct") + ls_connect("LS Elev 3", "Overworld Redux, Furnace_gyro_upper_north") + + # connect ls elevation regions to portals where you need to get behind the map to enter it + if options.ladder_storage >= LadderStorage.option_hard: + ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_") + ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_") + ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house") + ls_connect("LS Elev 5", "Overworld Redux, Temple_main") + + # connect the non-overworld ones + for ls_info in non_ow_ls_list: + # for places where the destination is a region (so you have to get knocked down) + if ls_info.dest_is_region: + # none of the non-ow ones have multiple ladders that can be used, so don't need has_any + if options.shuffle_ladders and ls_info.ladders_req: + regions[ls_info.origin].connect( + connecting_region=regions[ls_info.destination], + name=ls_info.destination + " (LS) " + ls_info.origin, + rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world) + and state.has(lad, player)) + else: + regions[ls_info.origin].connect( + connecting_region=regions[ls_info.destination], + name=ls_info.destination + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world)) continue - # soft lock if you don't have the ladder, I regret writing unrestricted logic - elif portal_name == "Temple Rafters Entrance" and not options.entrance_rando: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) - and (state.has("Ladder near Temple Rafters", player) - or (state.has_all({laurels, grapple}, player) - and ((state.has("Ladders near Patrol Cave", player) - and (state.has("Ladders near Dark Tomb", player) - or state.has("Ladder to Quarry", player) - and (state.has(fire_wand, player) or has_sword(state, player)))) - or state.has("Ladders near Overworld Checkpoint", player) - or has_ice_grapple_logic(True, state, world))))) - # if no ladder items are required, just do the basic stick only lambda - elif not ladders or not options.shuffle_ladders: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player)) - # one ladder required - elif len(ladders) == 1: - ladder = ladders.pop() - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) and state.has(ladder, player)) - # if multiple ladders can be used + + portal_name, dest_region = get_portal_info(ls_info.destination) + # these two are special cases + if ls_info.destination == "Atoll Redux, Frog Stairs_mouth": + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) + and (has_ladder("Ladders in South Atoll", state, world) + or state.has(key, player, 2) # can do it from the rope + # ice grapple push a crab into the door + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or options.ladder_storage >= LadderStorage.option_medium)) # use the little ladder + # holy cross mid-ls to get in here + elif ls_info.destination == "Swamp Redux 2, Cathedral Redux_secret": + if ls_info.origin == "Swamp Mid": + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world) + and has_ladder("Ladders in Swamp", state, world)) + else: + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world)) + + elif options.shuffle_ladders and ls_info.ladders_req: + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world) + and state.has(lad, player)) else: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)) + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world)) + + for region in ladder_regions.values(): + world.multiworld.regions.append(region) def set_er_location_rules(world: "TunicWorld") -> None: player = world.player - options = world.options forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) @@ -1439,10 +1326,13 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Ruined Atoll set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) + # ice grapple push a crab through the door set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Frog's Domain set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), @@ -1452,36 +1342,51 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) + # Library Lab + set_rule(world.get_location("Library Lab - Page 1"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player)) + # yes, you can clear the leaves with dagger + # gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have + # but really, I expect the player to just throw a bomb at them if they don't have melee + set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), + lambda state: has_stick(state, player) or state.has(ice_dagger, player)) # Beneath the Vault set_rule(world.get_location("Beneath the Fortress - Bridge"), - lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) + lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) # Quarry set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), lambda state: state.has(laurels, player)) # Ziggurat - # if ER is off, you still need to get past the Admin or you'll get stuck in lower zig + # if ER is off, while you can get the chest, you won't be able to actually get through zig set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), - lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player) - or options.entrance_rando))) + lambda state: has_sword(state, player) or (state.has(fire_wand, player) + and (state.has(laurels, player) + or world.options.entrance_rando))) set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), lambda state: has_sword(state, player) and has_ability(prayer, state, world)) # Bosses set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player)) - # nmg - kill Librarian with a lure, or gun I guess set_rule(world.get_location("Librarian - Hexagon Green"), - lambda state: (has_sword(state, player) or options.logic_rules) + lambda state: has_sword(state, player) and has_ladder("Ladders in Library", state, world)) - # nmg - kill boss scav with orb + firecracker, or similar + # can ice grapple boss scav off the side + # the grapple from the other side of the bridge isn't in logic 'cause we don't have a misc tricks option set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + lambda state: has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Swamp set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), @@ -1490,7 +1395,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) - # these two swamp checks really want you to kill the big skeleton first + # really hard to do 4 skulls with a big skeleton chasing you around set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) @@ -1541,7 +1446,13 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Bombable Walls for location_name in bomb_walls: - set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or can_shop(state, world)) + set_rule(world.get_location(location_name), + lambda state: state.has(gun, player) + or can_shop(state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) + # not enough space to ice grapple into here + set_rule(world.get_location("Quarry - [East] Bombable Wall"), + lambda state: state.has(gun, player) or can_shop(state, world)) # Shop set_rule(world.get_location("Shop - Potion 1"), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index e7c8fd58d0c6..05f6177aa57d 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,7 +1,7 @@ -from typing import Dict, List, Set, TYPE_CHECKING +from typing import Dict, List, Set, Tuple, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table -from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd +from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo from .er_rules import set_er_region_rules from Options import PlandoConnection from .options import EntranceRando @@ -22,17 +22,18 @@ class TunicERLocation(Location): def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} + for region_name, region_data in world.er_regions.items(): + regions[region_name] = Region(region_name, world.player, world.multiworld) + if world.options.entrance_rando: - portal_pairs = pair_portals(world) + portal_pairs = pair_portals(world, regions) + # output the entrances to the spoiler log here for convenience sorted_portal_pairs = sort_portals(portal_pairs) for portal1, portal2 in sorted_portal_pairs.items(): world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player) else: - portal_pairs = vanilla_portals() - - for region_name, region_data in tunic_er_regions.items(): - regions[region_name] = Region(region_name, world.player, world.multiworld) + portal_pairs = vanilla_portals(world, regions) set_er_region_rules(world, regions, portal_pairs) @@ -93,7 +94,18 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: region.locations.append(location) -def vanilla_portals() -> Dict[Portal, Portal]: +# all shops are the same shop. however, you cannot get to all shops from the same shop entrance. +# so, we need a bunch of shop regions that connect to the actual shop, but the actual shop cannot connect back +def create_shop_region(world: "TunicWorld", regions: Dict[str, Region]) -> None: + new_shop_name = f"Shop {world.shop_num}" + world.er_regions[new_shop_name] = RegionInfo("Shop", dead_end=DeadEnd.all_cats) + new_shop_region = Region(new_shop_name, world.player, world.multiworld) + new_shop_region.connect(regions["Shop"]) + regions[new_shop_name] = new_shop_region + world.shop_num += 1 + + +def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} # we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"] @@ -105,8 +117,9 @@ def vanilla_portals() -> Dict[Portal, Portal]: portal2_sdt = portal1.destination_scene() if portal2_sdt.startswith("Shop,"): - portal2 = Portal(name="Shop", region="Shop", + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", destination="Previous Region", tag="_") + create_shop_region(world, regions) elif portal2_sdt == "Purgatory, Purgatory_bottom": portal2_sdt = "Purgatory, Purgatory_top" @@ -125,14 +138,15 @@ def vanilla_portals() -> Dict[Portal, Portal]: # pairing off portals, starting with dead ends -def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: - # separate the portals into dead ends and non-dead ends +def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} dead_ends: List[Portal] = [] two_plus: List[Portal] = [] player_name = world.player_name portal_map = portal_mapping.copy() - logic_rules = world.options.logic_rules.value + laurels_zips = world.options.laurels_zips.value + ice_grappling = world.options.ice_grappling.value + ladder_storage = world.options.ladder_storage.value fixed_shop = world.options.fixed_shop laurels_location = world.options.laurels_location traversal_reqs = deepcopy(traversal_requirements) @@ -142,19 +156,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # if it's not one of the EntranceRando options, it's a custom seed if world.options.entrance_rando.value not in EntranceRando.options.values(): seed_group = world.seed_groups[world.options.entrance_rando.value] - logic_rules = seed_group["logic_rules"] + laurels_zips = seed_group["laurels_zips"] + ice_grappling = seed_group["ice_grappling"] + ladder_storage = seed_group["ladder_storage"] fixed_shop = seed_group["fixed_shop"] laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False + logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage) + # marking that you don't immediately have laurels if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"): has_laurels = False - shop_scenes: Set[str] = set() shop_count = 6 if fixed_shop: shop_count = 0 - shop_scenes.add("Overworld Redux") else: # if fixed shop is off, remove this portal for portal in portal_map: @@ -169,13 +185,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # create separate lists for dead ends and non-dead ends for portal in portal_map: - dead_end_status = tunic_er_regions[portal.region].dead_end + dead_end_status = world.er_regions[portal.region].dead_end if dead_end_status == DeadEnd.free: two_plus.append(portal) elif dead_end_status == DeadEnd.all_cats: dead_ends.append(portal) elif dead_end_status == DeadEnd.restricted: - if logic_rules: + if ice_grappling: two_plus.append(portal) else: dead_ends.append(portal) @@ -196,7 +212,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # make better start region stuff when/if implementing random start start_region = "Overworld" connected_regions.add(start_region) - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) if world.options.entrance_rando.value in EntranceRando.options.values(): plando_connections = world.options.plando_connections.value @@ -225,12 +241,14 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both")) non_dead_end_regions = set() - for region_name, region_info in tunic_er_regions.items(): + for region_name, region_info in world.er_regions.items(): if not region_info.dead_end: non_dead_end_regions.add(region_name) - elif region_info.dead_end == 2 and logic_rules: + # if ice grappling to places is in logic, both places stop being dead ends + elif region_info.dead_end == DeadEnd.restricted and ice_grappling: non_dead_end_regions.add(region_name) - elif region_info.dead_end == 3: + # secret gathering place and zig skip get weird, special handling + elif region_info.dead_end == DeadEnd.special: if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \ or (region_name == "Zig Skip Exit" and fixed_shop): non_dead_end_regions.add(region_name) @@ -239,6 +257,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: for connection in plando_connections: p_entrance = connection.entrance p_exit = connection.exit + # if you plando secret gathering place, need to know that during portal pairing + if "Secret Gathering Place Exit" in [p_entrance, p_exit]: + waterfall_plando = True portal1_dead_end = True portal2_dead_end = True @@ -285,16 +306,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: break # if it's not a dead end, it might be a shop if p_exit == "Shop Portal": - portal2 = Portal(name="Shop Portal", region="Shop", + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", destination="Previous Region", tag="_") + create_shop_region(world, regions) shop_count -= 1 # need to maintain an even number of portals total if shop_count < 0: shop_count += 2 - for p in portal_mapping: - if p.name == p_entrance: - shop_scenes.add(p.scene()) - break # and if it's neither shop nor dead end, it just isn't correct else: if not portal2: @@ -327,11 +345,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: else: raise Exception(f"{player_name} paired a dead end to a dead end in their " "plando connections.") - waterfall_plando = True portal_pairs[portal1] = portal2 # if we have plando connections, our connected regions may change somewhat - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"): portal1 = None @@ -343,7 +360,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: raise Exception(f"Failed to do Fixed Shop option. " f"Did {player_name} plando connection the Windmill Shop entrance?") - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", + destination="Previous Region", tag="_") + create_shop_region(world, regions) portal_pairs[portal1] = portal2 two_plus.remove(portal1) @@ -393,7 +412,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: if waterfall_plando: cr = connected_regions.copy() cr.add(portal.region) - if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_rules): + if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks): continue elif portal.region != "Secret Gathering Place": continue @@ -405,9 +424,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # once we have both portals, connect them and add the new region(s) to connected_regions if check_success == 2: - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if "Secret Gathering Place" in connected_regions: has_laurels = True + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) portal_pairs[portal1] = portal2 check_success = 0 random_object.shuffle(two_plus) @@ -418,16 +437,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: shop_count = 0 for i in range(shop_count): - portal1 = None - for portal in two_plus: - if portal.scene() not in shop_scenes: - shop_scenes.add(portal.scene()) - portal1 = portal - two_plus.remove(portal) - break + portal1 = two_plus.pop() if portal1 is None: - raise Exception("Too many shops in the pool, or something else went wrong.") - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + raise Exception("TUNIC: Too many shops in the pool, or something else went wrong.") + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", + destination="Previous Region", tag="_") + create_shop_region(world, regions) portal_pairs[portal1] = portal2 @@ -460,13 +475,12 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic region1 = regions[portal1.region] region2 = regions[portal2.region] region1.connect(connecting_region=region2, name=portal1.name) - # prevent the logic from thinking you can get to any shop-connected region from the shop - if portal2.name not in {"Shop", "Shop Portal"}: - region2.connect(connecting_region=region1, name=portal2.name) + region2.connect(connecting_region=region1, name=portal2.name) def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]], - has_laurels: bool, logic: int) -> Set[str]: + has_laurels: bool, logic: Tuple[bool, int, int]) -> Set[str]: + zips, ice_grapples, ls = logic # starting count, so we can run it again if this changes region_count = len(connected_regions) for origin, destinations in traversal_reqs.items(): @@ -485,11 +499,15 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s if req == "Hyperdash": if not has_laurels: break - elif req == "NMG": - if not logic: + elif req == "Zip": + if not zips: + break + # if req is higher than logic option, then it breaks since it's not a valid connection + elif req.startswith("IG"): + if int(req[-1]) > ice_grapples: break - elif req == "UR": - if logic < 2: + elif req.startswith("LS"): + if int(req[-1]) > ls: break elif req not in connected_regions: break diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 3e7f2c1a4382..55aa3468fc6b 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -166,6 +166,7 @@ class TunicItemData(NamedTuple): "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"), } +# items to be replaced by fool traps fool_tiers: List[List[str]] = [ [], ["Money x1", "Money x10", "Money x15", "Money x16"], @@ -173,6 +174,7 @@ class TunicItemData(NamedTuple): ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"], ] +# items we'll want the location of in slot data, for generating in-game hints slot_data_item_names = [ "Stick", "Sword", @@ -235,9 +237,10 @@ def get_item_group(item_name: str) -> str: "Questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"}, "Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't "Ladders to Bell": {"Ladders to West Bell"}, - "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell + "Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided Ladders in Well was Ladders to West Bell "Ladders in Atoll": {"Ladders in South Atoll"}, "Ladders in Ruined Atoll": {"Ladders in South Atoll"}, + "Ladders in Town": {"Ladders in Overworld Town"}, # fuzzy matching decided this was Ladders in South Atoll } item_name_groups.update(extra_groups) diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py new file mode 100644 index 000000000000..a29d50b4f455 --- /dev/null +++ b/worlds/tunic/ladder_storage_data.py @@ -0,0 +1,186 @@ +from typing import Dict, List, Set, NamedTuple, Optional + + +# ladders in overworld, since it is the most complex area for ladder storage +class OWLadderInfo(NamedTuple): + ladders: Set[str] # ladders where the top or bottom is at the same elevation + portals: List[str] # portals at the same elevation, only those without doors + regions: List[str] # regions where a melee enemy can hit you out of ladder storage + + +# groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets +ow_ladder_groups: Dict[str, OWLadderInfo] = { + # lowest elevation + "LS Elev 0": OWLadderInfo({"Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"}, + ["Swamp Redux 2_conduit", "Overworld Cave_", "Atoll Redux_lower", "Maze Room_", + "Town Basement_beach", "Archipelagos Redux_lower", "Archipelagos Redux_lowest"], + ["Overworld Beach"]), + # also the east filigree room + "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"}, + ["Furnace_gyro_lower", "Swamp Redux 2_wall"], + ["Overworld Tunnel Turret"]), + # also the fountain filigree room and ruined passage door + "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"}, + ["Archipelagos Redux_upper", "Ruins Passage_east"], + ["After Ruined Passage"]), + # also old house door + "LS Elev 3": OWLadderInfo({"Ladders near Weathervane", "Ladder to Quarry", "Ladders to West Bell", + "Ladders in Overworld Town"}, + [], + ["Overworld after Envoy", "East Overworld"]), + # skip top of top ladder next to weathervane level, does not provide logical access to anything + "LS Elev 4": OWLadderInfo({"Ladders near Dark Tomb", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Well", + "Ladders in Overworld Town"}, + ["Darkwoods Tunnel_"], + []), + "LS Elev 5": OWLadderInfo({"Ladders near Overworld Checkpoint", "Ladders near Patrol Cave"}, + ["PatrolCave_", "Forest Belltower_", "Fortress Courtyard_", "ShopSpecial_"], + ["East Overworld"]), + # skip top of belltower, middle of dark tomb ladders, and top of checkpoint, does not grant access to anything + "LS Elev 6": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters"}, + ["Temple_rafters"], + ["Overworld above Patrol Cave"]), + # in-line with the chest above dark tomb, gets you up the mountain stairs + "LS Elev 7": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters", "Ladders near Dark Tomb"}, + ["Mountain_"], + ["Upper Overworld"]), +} + + +# ladders accessible within different regions of overworld, only those that are relevant +# other scenes will just have them hardcoded since this type of structure is not necessary there +region_ladders: Dict[str, Set[str]] = { + "Overworld": {"Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb", + "Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"}, + "Overworld Beach": {"Ladder to Ruined Atoll"}, + "Overworld at Patrol Cave": {"Ladders near Patrol Cave"}, + "Overworld Quarry Entry": {"Ladder to Quarry"}, + "Overworld Belltower": {"Ladders to West Bell"}, + "Overworld after Temple Rafters": {"Ladders near Temple Rafters"}, +} + + +class LadderInfo(NamedTuple): + origin: str # origin region + destination: str # destination portal + ladders_req: Optional[str] = None # ladders required to do this + dest_is_region: bool = False # whether it is a region that you are going to + + +easy_ls: List[LadderInfo] = [ + # In the furnace + # Furnace ladder to the fuse entrance + LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north"), + # Furnace ladder to Dark Tomb + LadderInfo("Furnace Ladder Area", "Furnace, Crypt Redux_"), + # Furnace ladder to the West Garden connector + LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west"), + + # West Garden + # exit after Garden Knight + LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_upper"), + # West Garden laurels exit + LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_lowest"), + + # Atoll, use the little ladder you fix at the beginning + LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"), + LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth"), # special case + + # East Forest + # Entrance by the dancing fox holy cross spot + LadderInfo("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper"), + + # From the west side of Guard House 1 to the east side + LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate"), + LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_"), + + # Fortress Exterior + # shop, ls at the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_"), + # Fortress main entry and grave path lower entry, ls at the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door"), + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower"), + # Use the top of the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper"), + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_"), + + # same as above, except from the east side of the area + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower"), + + # same as above, except from the Beneath the Vault entrance ladder + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door", + "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower", + "Ladder to Beneath the Vault"), + + # Swamp to Gauntlet + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", "Ladders in Swamp"), + + # Ladder by the hero grave + LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Shop_"), +] + +# if we can gain elevation or get knocked down, add the harder ones +medium_ls: List[LadderInfo] = [ + # region-destination versions of easy ls spots + LadderInfo("East Forest", "East Forest Dance Fox Spot", dest_is_region=True), + # fortress courtyard knockdowns are never logically relevant, the fuse requires upper + LadderInfo("Back of Swamp", "Swamp Mid", dest_is_region=True), + LadderInfo("Back of Swamp", "Swamp Front", dest_is_region=True), + + # gain height off the northeast fuse ramp + LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_eye"), + + # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch + LadderInfo("Forest Grave Path Main", "Sword Access, East Forest Redux_upper"), + + # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard Upper", dest_is_region=True), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard Upper", dest_is_region=True), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper", + "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard Upper", "Ladder to Beneath the Vault", + dest_is_region=True), + + # need to gain height to get up the stairs + LadderInfo("Lower Mountain", "Mountain, Mountaintop_"), + + # Where the rope is behind Monastery + LadderInfo("Quarry Entry", "Quarry Redux, Monastery_back"), + LadderInfo("Quarry Monastery Entry", "Quarry Redux, Monastery_back"), + LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"), + + LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_2_"), + LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Front", dest_is_region=True), + + # Swamp to Overworld upper + LadderInfo("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_wall"), +] + +hard_ls: List[LadderInfo] = [ + # lower ladder, go into the waterfall then above the bonfire, up a ramp, then through the right wall + LadderInfo("Beneath the Well Front", "Sewer, Sewer_Boss_", "Ladders in Well"), + LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"), + LadderInfo("Beneath the Well Front", "Beneath the Well Back", "Ladders in Well", dest_is_region=True), + # go through the hexagon engraving above the vault door + LadderInfo("Frog's Domain", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"), + # the turret at the end here is not affected by enemy rando + LadderInfo("Frog's Domain", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True), + # todo: see if we can use that new laurels strat here + # LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"), + # go behind the cathedral to reach the door, pretty easily doable + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_main", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_main"), + # need to do hc midair, probably cannot get into this without hc + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_secret", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret"), +] diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 09916228163d..442e0c01446d 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -47,7 +47,7 @@ class TunicLocationData(NamedTuple): "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"), "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="Holy Cross"), "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="Holy Cross"), - "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "Lower Forest"), "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"), "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"), "East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"), @@ -205,7 +205,7 @@ class TunicLocationData(NamedTuple): "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"), "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"), "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"), - "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), + "Monastery - Monastery Chest": TunicLocationData("Monastery", "Monastery Back"), "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"), @@ -224,7 +224,7 @@ class TunicLocationData(NamedTuple): "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), - "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), + "Hero's Grave - Ash Relic": TunicLocationData("Monastery", "Hero Relic - Quarry"), "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 92cbafba233f..1683b3ca5aee 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Dict, Any from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, - PerGameCommonOptions, OptionGroup) + PerGameCommonOptions, OptionGroup, Visibility) from .er_data import portal_mapping @@ -39,27 +39,6 @@ class AbilityShuffling(Toggle): display_name = "Shuffle Abilities" -class LogicRules(Choice): - """ - Set which logic rules to use for your world. - Restricted: Standard logic, no glitches. - No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic. - * Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer. - Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early. - * Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic. - * Using Ladder Storage to get to individual chests is not in logic to avoid tedium. - * Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on. - """ - internal_name = "logic_rules" - display_name = "Logic Rules" - option_restricted = 0 - option_no_major_glitches = 1 - alias_nmg = 1 - option_unrestricted = 2 - alias_ur = 2 - default = 0 - - class Lanternless(Toggle): """ Choose whether you require the Lantern for dark areas. @@ -173,8 +152,8 @@ class ShuffleLadders(Toggle): """ internal_name = "shuffle_ladders" display_name = "Shuffle Ladders" - - + + class TunicPlandoConnections(PlandoConnections): """ Generic connection plando. Format is: @@ -189,6 +168,82 @@ class TunicPlandoConnections(PlandoConnections): duplicate_exits = True +class LaurelsZips(Toggle): + """ + Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots. + Notable inclusions are the Monastery gate, Ruined Passage door, Old House gate, Forest Grave Path gate, and getting from the Back of Swamp to the Middle of Swamp. + """ + internal_name = "laurels_zips" + display_name = "Laurels Zips Logic" + + +class IceGrappling(Choice): + """ + Choose whether grappling frozen enemies is in logic. + Easy includes ice grappling enemies that are in range without luring them. May include clips through terrain. + Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it. + Hard includes luring or grappling enemies to get to where you want to go. + The Medium and Hard options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Note: You will still be expected to ice grapple to the slime in East Forest from below with this option off. + """ + internal_name = "ice_grappling" + display_name = "Ice Grapple Logic" + option_off = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + default = 0 + + +class LadderStorage(Choice): + """ + Choose whether Ladder Storage is in logic. + Easy includes uses of Ladder Storage to get to open doors over a long distance without too much difficulty. May include convenient elevation changes (going up Mountain stairs, stairs in front of Special Shop, etc.). + Medium includes the above as well as changing your elevation using the environment and getting knocked down by melee enemies mid-LS. + Hard includes the above as well as going behind the map to enter closed doors from behind, shooting a fuse with the magic wand to knock yourself down at close range, and getting into the Cathedral Secret Legend room mid-LS. + Enabling any of these difficulty options will give the player the Torch item to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Opening individual chests while doing ladder storage is excluded due to tedium. + Knocking yourself out of LS with a bomb is excluded due to the problematic nature of consumables in logic. + """ + internal_name = "ladder_storage" + display_name = "Ladder Storage Logic" + option_off = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + default = 0 + + +class LadderStorageWithoutItems(Toggle): + """ + If disabled, you logically require Stick, Sword, or Magic Orb to perform Ladder Storage. + If enabled, you will be expected to perform Ladder Storage without progression items. + This can be done with the plushie code, a Golden Coin, Prayer, and many other options. + + This option has no effect if you do not have Ladder Storage Logic enabled. + """ + internal_name = "ladder_storage_without_items" + display_name = "Ladder Storage without Items" + + +class LogicRules(Choice): + """ + This option has been superseded by the individual trick options. + If set to nmg, it will set Ice Grappling to medium and Laurels Zips on. + If set to ur, it will do nmg as well as set Ladder Storage to medium. + It is here to avoid breaking old yamls, and will be removed at a later date. + """ + visibility = Visibility.none + internal_name = "logic_rules" + display_name = "Logic Rules" + option_restricted = 0 + option_no_major_glitches = 1 + alias_nmg = 1 + option_unrestricted = 2 + alias_ur = 2 + default = 0 + + @dataclass class TunicOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool @@ -199,22 +254,30 @@ class TunicOptions(PerGameCommonOptions): shuffle_ladders: ShuffleLadders entrance_rando: EntranceRando fixed_shop: FixedShop - logic_rules: LogicRules fool_traps: FoolTraps hexagon_quest: HexagonQuest hexagon_goal: HexagonGoal extra_hexagon_percentage: ExtraHexagonPercentage + laurels_location: LaurelsLocation lanternless: Lanternless maskless: Maskless - laurels_location: LaurelsLocation + laurels_zips: LaurelsZips + ice_grappling: IceGrappling + ladder_storage: LadderStorage + ladder_storage_without_items: LadderStorageWithoutItems plando_connections: TunicPlandoConnections + + logic_rules: LogicRules tunic_option_groups = [ OptionGroup("Logic Options", [ - LogicRules, Lanternless, Maskless, + LaurelsZips, + IceGrappling, + LadderStorage, + LadderStorageWithoutItems ]) ] @@ -231,9 +294,12 @@ class TunicOptions(PerGameCommonOptions): "Glace Mode": { "accessibility": "minimal", "ability_shuffling": True, - "entrance_rando": "yes", + "entrance_rando": True, "fool_traps": "onslaught", - "logic_rules": "unrestricted", + "laurels_zips": True, + "ice_grappling": "hard", + "ladder_storage": "hard", + "ladder_storage_without_items": True, "maskless": True, "lanternless": True, }, diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py index c30a44bb8ff6..93ec5640e0c2 100644 --- a/worlds/tunic/regions.py +++ b/worlds/tunic/regions.py @@ -16,7 +16,8 @@ "Eastern Vault Fortress": {"Beneath the Vault"}, "Beneath the Vault": {"Eastern Vault Fortress"}, "Quarry Back": {"Quarry"}, - "Quarry": {"Lower Quarry"}, + "Quarry": {"Monastery", "Lower Quarry"}, + "Monastery": set(), "Lower Quarry": {"Rooted Ziggurat"}, "Rooted Ziggurat": set(), "Swamp": {"Cathedral"}, diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 68756869038d..aa69666daeb6 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -3,7 +3,7 @@ from worlds.generic.Rules import set_rule, forbid_item, add_rule from BaseClasses import CollectionState -from .options import TunicOptions +from .options import TunicOptions, LadderStorage, IceGrappling if TYPE_CHECKING: from . import TunicWorld @@ -27,10 +27,10 @@ blue_hexagon = "Blue Questagon" gold_hexagon = "Gold Questagon" +# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall", "Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain", - "Quarry - [West] Upper Area Bombable Wall", "Quarry - [East] Bombable Wall", - "Ruined Atoll - [Northwest] Bombable Wall"] + "Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"] def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: @@ -64,32 +64,33 @@ def has_sword(state: CollectionState, player: int) -> bool: return state.has("Sword", player) or state.has("Sword Upgrade", player, 2) -def has_ice_grapple_logic(long_range: bool, state: CollectionState, world: "TunicWorld") -> bool: - player = world.player - if not world.options.logic_rules: +def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.laurels_zips and state.has(laurels, world.player) + + +def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool: + if world.options.ice_grappling < difficulty: return False if not long_range: - return state.has_all({ice_dagger, grapple}, player) + return state.has_all({ice_dagger, grapple}, world.player) else: - return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world) + return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world) def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: - return world.options.logic_rules == "unrestricted" and has_stick(state, world.player) + if not world.options.ladder_storage: + return False + if world.options.ladder_storage_without_items: + return True + return has_stick(state, world.player) or state.has(grapple, world.player) def has_mask(state: CollectionState, world: "TunicWorld") -> bool: - if world.options.maskless: - return True - else: - return state.has(mask, world.player) + return world.options.maskless or state.has(mask, world.player) def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: - if world.options.lanternless: - return True - else: - return state.has(lantern, world.player) + return world.options.lanternless or state.has(lantern, world.player) def set_region_rules(world: "TunicWorld") -> None: @@ -102,16 +103,20 @@ def set_region_rules(world: "TunicWorld") -> None: lambda state: has_stick(state, player) or state.has(fire_wand, player) world.get_entrance("Overworld -> Dark Tomb").access_rule = \ lambda state: has_lantern(state, world) + # laurels in, ladder storage in through the furnace, or ice grapple down the belltower world.get_entrance("Overworld -> West Garden").access_rule = \ - lambda state: state.has(laurels, player) \ - or can_ladder_storage(state, world) + lambda state: (state.has(laurels, player) + or can_ladder_storage(state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \ lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, state, world) \ + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) \ or can_ladder_storage(state, world) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ - lambda state: has_lantern(state, world) and has_ability(prayer, state, world) + lambda state: (has_lantern(state, world) and has_ability(prayer, state, world) + # there's some boxes in the way + and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player))) world.get_entrance("Ruined Atoll -> Library").access_rule = \ lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) world.get_entrance("Overworld -> Quarry").access_rule = \ @@ -124,8 +129,8 @@ def set_region_rules(world: "TunicWorld") -> None: world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \ lambda state: state.has(grapple, player) and has_ability(prayer, state, world) world.get_entrance("Swamp -> Cathedral").access_rule = \ - lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \ - or has_ice_grapple_logic(False, state, world) + lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) \ + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) world.get_entrance("Overworld -> Spirit Arena").access_rule = \ lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player) @@ -133,10 +138,18 @@ def set_region_rules(world: "TunicWorld") -> None: and has_ability(prayer, state, world) and has_sword(state, player) and state.has_any({lantern, laurels}, player)) + world.get_region("Quarry").connect(world.get_region("Rooted Ziggurat"), + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world) + and has_ability(prayer, state, world)) + + if options.ladder_storage >= LadderStorage.option_medium: + # ls at any ladder in a safe spot in quarry to get to the monastery rope entrance + world.get_region("Quarry Back").connect(world.get_region("Monastery"), + rule=lambda state: can_ladder_storage(state, world)) + def set_location_rules(world: "TunicWorld") -> None: player = world.player - options = world.options forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) @@ -147,11 +160,13 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: has_ability(prayer, state, world) or state.has(laurels, player) or can_ladder_storage(state, world) - or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world))) + or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) + and has_lantern(state, world))) set_rule(world.get_location("Fortress Courtyard - Page Near Cave"), lambda state: has_ability(prayer, state, world) or state.has(laurels, player) or can_ladder_storage(state, world) - or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world))) + or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) + and has_lantern(state, world))) set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), lambda state: has_ability(holy_cross, state, world)) set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), @@ -186,17 +201,17 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Old House - Normal Chest"), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world)) set_rule(world.get_location("Old House - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world) and ( state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world))) set_rule(world.get_location("Old House - Shield Pickup"), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world)) set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) set_rule(world.get_location("Overworld - [Southwest] From West Garden"), @@ -206,7 +221,7 @@ def set_location_rules(world: "TunicWorld") -> None: or (has_lantern(state, world) and has_sword(state, player)) or can_ladder_storage(state, world)) set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"), - lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) + lambda state: state.has_any({grapple, laurels}, player)) set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) set_rule(world.get_location("Special Shop - Secret Page Pickup"), @@ -215,11 +230,11 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: has_ability(holy_cross, state, world) and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, world))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Sealed Temple - Page Pickup"), lambda state: state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("West Furnace - Lantern Pickup"), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) @@ -254,7 +269,7 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"), @@ -265,12 +280,15 @@ def set_location_rules(world: "TunicWorld") -> None: # Ruined Atoll set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) + # ice grapple push a crab through the door set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Librarian - Hexagon Green"), - lambda state: has_sword(state, player) or options.logic_rules) + lambda state: has_sword(state, player)) # Frog's Domain set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), @@ -280,15 +298,28 @@ def set_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) + # Library Lab + set_rule(world.get_location("Library Lab - Page 1"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress + # yes, you can clear the leaves with dagger + # gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have + # but really, I expect the player to just throw a bomb at them if they don't have melee set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), - lambda state: state.has(laurels, player)) + lambda state: state.has(laurels, player) and (has_stick(state, player) or state.has(ice_dagger, player))) set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) # Beneath the Vault set_rule(world.get_location("Beneath the Fortress - Bridge"), @@ -301,14 +332,14 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player)) - # nmg - kill boss scav with orb + firecracker, or similar set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + lambda state: has_sword(state, player)) # Swamp set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) - and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) + and (state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), @@ -335,8 +366,16 @@ def set_location_rules(world: "TunicWorld") -> None: # Bombable Walls for location_name in bomb_walls: # has_sword is there because you can buy bombs in the shop - set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or has_sword(state, player)) + set_rule(world.get_location(location_name), + lambda state: state.has(gun, player) + or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) add_rule(world.get_location("Cube Cave - Holy Cross Chest"), + lambda state: state.has(gun, player) + or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) + # can't ice grapple to this one, not enough space + set_rule(world.get_location("Quarry - [East] Bombable Wall"), lambda state: state.has(gun, player) or has_sword(state, player)) # Shop diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index 72d4a498d1ee..bbceb7468ff3 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -68,3 +68,57 @@ def test_overworld_hc_chest(self) -> None: self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) self.collect_by_name(["Pages 42-43 (Holy Cross)"]) self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) + + +class TestERSpecial(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false, + options.FixedShop.internal_name: options.FixedShop.option_false, + options.IceGrappling.internal_name: options.IceGrappling.option_easy, + "plando_connections": [ + { + "entrance": "Stick House Entrance", + "exit": "Ziggurat Portal Room Entrance" + }, + { + "entrance": "Ziggurat Lower to Ziggurat Tower", + "exit": "Secret Gathering Place Exit" + } + ]} + # with these plando connections, you need to ice grapple from the back of lower zig to the front to get laurels + + +# ensure that ladder storage connections connect to the outlet region, not the portal's region +class TestLadderStorage(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false, + options.FixedShop.internal_name: options.FixedShop.option_false, + options.LadderStorage.internal_name: options.LadderStorage.option_hard, + options.LadderStorageWithoutItems.internal_name: options.LadderStorageWithoutItems.option_false, + "plando_connections": [ + { + "entrance": "Fortress Courtyard Shop", + # "exit": "Ziggurat Portal Room Exit" + "exit": "Spawn to Far Shore" + }, + { + "entrance": "Fortress Courtyard to Beneath the Vault", + "exit": "Stick House Exit" + }, + { + "entrance": "Stick House Entrance", + "exit": "Fortress Courtyard to Overworld" + }, + { + "entrance": "Old House Waterfall Entrance", + "exit": "Ziggurat Portal Room Entrance" + }, + ]} + + def test_ls_to_shop_entrance(self) -> None: + self.collect_by_name(["Magic Orb"]) + self.assertFalse(self.can_reach_location("Fortress Courtyard - Page Near Cave")) + self.collect_by_name(["Pages 24-25 (Prayer)"]) + self.assertTrue(self.can_reach_location("Fortress Courtyard - Page Near Cave")) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 9993e42fdc1a..c9848f2ffe47 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -61,7 +61,7 @@ class WitnessWorld(World): item_name_groups = static_witness_items.ITEM_GROUPS location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS - required_client_version = (0, 4, 5) + required_client_version = (0, 5, 1) player_logic: WitnessPlayerLogic player_locations: WitnessPlayerLocations @@ -204,11 +204,10 @@ def create_regions(self) -> None: ] 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 @@ def create_regions(self) -> None: 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.""" ) diff --git a/worlds/witness/data/WitnessLogicVariety.txt b/worlds/witness/data/WitnessLogicVariety.txt new file mode 100644 index 000000000000..a3c388dfb1e4 --- /dev/null +++ b/worlds/witness/data/WitnessLogicVariety.txt @@ -0,0 +1,1223 @@ +==Tutorial (Inside)== + +Menu (Menu) - Entry - True: + +Entry (Entry): + +Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: +158000 - 0x00064 (Straight) - True - True +159510 - 0x01848 (EP) - 0x00064 - True + +Tutorial First Hallway Room (Tutorial First Hallway) - Tutorial - 0x00182: +158001 - 0x00182 (Bend) - True - True + +Tutorial (Tutorial) - Outside Tutorial - True: +158002 - 0x00293 (Front Center) - True - Dots +158003 - 0x00295 (Center Left) - 0x00293 - Black/White Squares & Colored Squares +158004 - 0x002C2 (Front Left) - 0x00295 - Stars +158005 - 0x0A3B5 (Back Left) - True - True +158006 - 0x0A3B2 (Back Right) - True - True +158007 - 0x03629 (Gate Open) - 0x002C2 & 0x0A3B5 & 0x0A3B2 - True +158008 - 0x03505 (Gate Close) - 0x2FAF6 & 0x03629 - True +158009 - 0x0C335 (Pillar) - True - Triangles +158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots +159512 - 0x33530 (Cloud EP) - True - True +159513 - 0x33600 (Patio Flowers EP) - 0x0C373 - True +159517 - 0x3352F (Gate EP) - 0x03505 - True + +==Tutorial (Outside)== + +Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2 - Outside Tutorial Vault - 0x033D0: +158650 - 0x033D4 (Vault Panel) - True - Dots & Black/White Squares & Colored Squares & Symmetry +Door - 0x033D0 (Vault Door) - 0x033D4 +158013 - 0x0005D (Shed Row 1) - True - Dots & Full Dots & Black/White Squares & Colored Squares +158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots & Full Dots & Stars +158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots & Full Dots & Shapers & Negative Shapers +158016 - 0x00060 (Shed Row 4) - 0x0005F - Dots & Full Dots & Black/White Squares & Stars & Stars + Same Colored Symbol & Eraser +158017 - 0x00061 (Shed Row 5) - 0x00060 - Dots & Full Dots & Triangles +158018 - 0x018AF (Tree Row 1) - True - Arrows +158019 - 0x0001B (Tree Row 2) - 0x018AF - Arrows +158020 - 0x012C9 (Tree Row 3) - 0x0001B - Arrows +158021 - 0x0001C (Tree Row 4) - 0x012C9 - Arrows & Black/White Squares & Colored Squares +158022 - 0x0001D (Tree Row 5) - 0x0001C - Arrows & Black/White Squares & Colored Squares +158023 - 0x0001E (Tree Row 6) - 0x0001D - Arrows & Black/White Squares & Colored Squares +158024 - 0x0001F (Tree Row 7) - 0x0001E - Arrows & Black/White Squares & Colored Squares +158025 - 0x00020 (Tree Row 8) - 0x0001F - Arrows & Black/White Squares & Colored Squares +158026 - 0x00021 (Tree Row 9) - 0x00020 - Arrows & Black/White Squares & Colored Squares +Door - 0x03BA2 (Outpost Path) - 0x0A3B5 +159511 - 0x03D06 (Garden EP) - True - True +159514 - 0x28A2F (Town Sewer EP) - True - True +159516 - 0x334A3 (Path EP) - True - True +159500 - 0x035C7 (Tractor EP) - True - True + +Outside Tutorial Vault (Outside Tutorial): +158651 - 0x03481 (Vault Box) - True - True + +Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: +158011 - 0x0A171 (Outpost Entry Panel) - True - Dots & Full Dots & Triangles & Black/White Squares +Door - 0x0A170 (Outpost Entry) - 0x0A171 + +Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3: +158012 - 0x04CA4 (Outpost Exit Panel) - True - Dots & Full Dots & Triangles & Black/White Squares +Door - 0x04CA3 (Outpost Exit) - 0x04CA4 +158600 - 0x17CFB (Discard) - True - Arrows & Triangles + +Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307: +158071 - 0x00143 (Apple Tree 1) - True - True +158072 - 0x0003B (Apple Tree 2) - 0x00143 - True +158073 - 0x00055 (Apple Tree 3) - 0x0003B - True +Door - 0x03307 (First Gate) - 0x00055 + +Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313: +158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True +158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True +Door - 0x03313 (Second Gate) - 0x032FF + +Orchard End (Orchard): + +Main Island (Main Island) - Outside Tutorial - True: +159801 - 0xFFD00 (Reached Independently) - True - True + +==Glass Factory== + +Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: +158027 - 0x01A54 (Entry Panel) - True - Symmetry +Door - 0x01A29 (Entry) - 0x01A54 +158601 - 0x3C12B (Discard) - True - Arrows & Triangles +159002 - 0x28B8A (Vase EP) - 0x01A54 - True + +Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED: +158028 - 0x00086 (Back Wall 1) - True - Symmetry +158029 - 0x00087 (Back Wall 2) - 0x00086 - Symmetry +158030 - 0x00059 (Back Wall 3) - 0x00087 - Symmetry +158031 - 0x00062 (Back Wall 4) - 0x00059 - Symmetry +158032 - 0x0005C (Back Wall 5) - 0x00062 - Symmetry +158033 - 0x0008D (Front 1) - 0x0005C - Symmetry & Dots +158034 - 0x00081 (Front 2) - 0x0008D - Symmetry & Dots +158035 - 0x00083 (Front 3) - 0x00081 - Symmetry & Dots +158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry & Dots +158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry & Dots +158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry & Dots +Door - 0x0D7ED (Back Wall) - 0x0005C + +Inside Glass Factory Behind Back Wall (Glass Factory) - The Ocean - 0x17CC8: +158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat + +==Symmetry Island== + +Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: +158040 - 0x000B0 (Lower Panel) - 0x0343A - Dots +Door - 0x17F3E (Lower) - 0x000B0 + +Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269: +158041 - 0x00022 (Right 1) - True - Symmetry & Dots & Full Dots & Triangles +158042 - 0x00023 (Right 2) - 0x00022 - Symmetry & Dots & Full Dots & Triangles +158043 - 0x00024 (Right 3) - 0x00023 - Symmetry & Dots & Full Dots & Triangles +158044 - 0x00025 (Right 4) - 0x00024 - Symmetry & Dots & Full Dots & Triangles +158045 - 0x00026 (Right 5) - 0x00025 - Symmetry & Dots & Full Dots & Triangles +158046 - 0x0007C (Back 1) - 0x00026 - Symmetry & Dots & Colored Dots +158047 - 0x0007E (Back 2) - 0x0007C - Symmetry & Dots & Colored Dots +158048 - 0x00075 (Back 3) - 0x0007E - Symmetry & Dots & Colored Dots +158049 - 0x00073 (Back 4) - 0x00075 - Symmetry & Dots & Colored Dots & Eraser +158050 - 0x00077 (Back 5) - 0x00073 - Symmetry & Dots & Colored Dots & Eraser +158051 - 0x00079 (Back 6) - 0x00077 - Symmetry & Dots & Colored Dots & Eraser +158052 - 0x00065 (Left 1) - 0x00079 - Symmetry & Dots & Colored Dots +158053 - 0x0006D (Left 2) - 0x00065 - Symmetry & Colored Squares +158054 - 0x00072 (Left 3) - 0x0006D - Symmetry & Stars +158055 - 0x0006F (Left 4) - 0x00072 - Symmetry & Stars & Stars + Same Colored Symbol & Colored Squares +158056 - 0x00070 (Left 5) - 0x0006F - Symmetry & Stars & Stars + Same Colored Symbol & Colored Squares +158057 - 0x00071 (Left 6) - 0x00070 - Symmetry & Colored Dots & Eraser +158058 - 0x00076 (Left 7) - 0x00071 - Symmetry & Dots & Eraser +158059 - 0x009B8 (Scenery Outlines 1) - True - Symmetry +158060 - 0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry +158061 - 0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry +158062 - 0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry +158063 - 0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry +158064 - 0x1C349 (Upper Panel) - 0x00076 - Symmetry & Dots +Door - 0x18269 (Upper) - 0x1C349 +159000 - 0x0332B (Glass Factory Black Line Reflection EP) - True - True + +Symmetry Island Upper (Symmetry Island): +158065 - 0x00A52 (Laser Yellow 1) - True - Symmetry & Colored Dots +158066 - 0x00A57 (Laser Yellow 2) - 0x00A52 - Symmetry & Colored Dots +158067 - 0x00A5B (Laser Yellow 3) - 0x00A57 - Symmetry & Colored Dots +158068 - 0x00A61 (Laser Blue 1) - 0x00A52 - Symmetry & Colored Dots +158069 - 0x00A64 (Laser Blue 2) - 0x00A61 & 0x00A57 - Symmetry & Colored Dots +158070 - 0x00A68 (Laser Blue 3) - 0x00A64 & 0x00A5B - Symmetry & Colored Dots +158700 - 0x0360D (Laser Panel) - 0x00A68 - True +Laser - 0x00509 (Laser) - 0x0360D +159001 - 0x03367 (Glass Factory Black Line EP) - True - True + +==Desert== + +Desert Obelisk (Desert) - Entry - True: +159700 - 0xFFE00 (Obelisk Side 1) - 0x0332B & 0x03367 & 0x28B8A - True +159701 - 0xFFE01 (Obelisk Side 2) - 0x037B6 & 0x037B2 & 0x000F7 - True +159702 - 0xFFE02 (Obelisk Side 3) - 0x3351D - True +159703 - 0xFFE03 (Obelisk Side 4) - 0x0053C & 0x00771 & 0x335C8 & 0x335C9 & 0x337F8 & 0x037BB & 0x220E4 & 0x220E5 - True +159704 - 0xFFE04 (Obelisk Side 5) - 0x334B9 & 0x334BC & 0x22106 & 0x0A14C & 0x0A14D - True +159709 - 0x00359 (Obelisk) - True - True + +Desert Outside (Desert) - Main Island - True - Desert Light Room - 0x09FEE - Desert Vault - 0x03444: +158652 - 0x0CC7B (Vault Panel) - True - Dots & Full Dots & Rotated Shapers & Negative Shapers & Stars & Stars + Same Colored Symbol +Door - 0x03444 (Vault Door) - 0x0CC7B +158602 - 0x17CE7 (Discard) - True - Arrows & Triangles +158076 - 0x00698 (Surface 1) - True - True +158077 - 0x0048F (Surface 2) - 0x00698 - True +158078 - 0x09F92 (Surface 3) - 0x0048F & 0x09FA0 - True +158079 - 0x09FA0 (Surface 3 Control) - 0x0048F - True +158080 - 0x0A036 (Surface 4) - 0x09F92 - True +158081 - 0x09DA6 (Surface 5) - 0x09F92 - True +158082 - 0x0A049 (Surface 6) - 0x09F92 - True +158083 - 0x0A053 (Surface 7) - 0x0A036 & 0x09DA6 & 0x0A049 - True +158084 - 0x09F94 (Surface 8) - 0x0A053 & 0x09F86 - True +158085 - 0x09F86 (Surface 8 Control) - 0x0A053 - True +158086 - 0x0C339 (Light Room Entry Panel) - 0x09F94 - True +Door - 0x09FEE (Light Room Entry) - 0x0C339 +158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True +Laser - 0x012FB (Laser) - 0x03608 +159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True +159020 - 0x3351D (Sand Snake EP) - True - True +159030 - 0x0053C (Facade Right EP) - True - True +159031 - 0x00771 (Facade Left EP) - True - True +159032 - 0x335C8 (Stairs Left EP) - True - True +159033 - 0x335C9 (Stairs Right EP) - True - True +159036 - 0x220E4 (Broken Wall Straight EP) - True - True +159037 - 0x220E5 (Broken Wall Bend EP) - True - True +159040 - 0x334B9 (Shore EP) - True - True +159041 - 0x334BC (Island EP) - True - True + +Desert Vault (Desert): +158653 - 0x0339E (Vault Box) - True - True + +Desert Light Room (Desert) - Desert Pond Room - 0x0C2C3: +158087 - 0x09FAA (Light Control) - True - True +158088 - 0x00422 (Light Room 1) - 0x09FAA - True +158089 - 0x006E3 (Light Room 2) - 0x09FAA - True +158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - True +Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D + +Desert Pond Room (Desert) - Desert Flood Room - 0x0A24B: +158091 - 0x00C72 (Pond Room 1) - True - True +158092 - 0x0129D (Pond Room 2) - 0x00C72 - True +158093 - 0x008BB (Pond Room 3) - 0x0129D - True +158094 - 0x0078D (Pond Room 4) - 0x008BB - True +158095 - 0x18313 (Pond Room 5) - 0x0078D - True +158096 - 0x0A249 (Flood Room Entry Panel) - 0x18313 - True +Door - 0x0A24B (Flood Room Entry) - 0x0A249 +159043 - 0x0A14C (Pond Room Near Reflection EP) - True - True +159044 - 0x0A14D (Pond Room Far Reflection EP) - True - True + +Desert Flood Room (Desert) - Desert Elevator Room - 0x0C316: +158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True +158098 - 0x1831E (Reduce Water Level Far Right) - True - True +158099 - 0x1C260 (Reduce Water Level Near Left) - True - True +158100 - 0x1831C (Reduce Water Level Near Right) - True - True +158101 - 0x1C2F3 (Raise Water Level Far Left) - True - True +158102 - 0x1831D (Raise Water Level Far Right) - True - True +158103 - 0x1C2B1 (Raise Water Level Near Left) - True - True +158104 - 0x1831B (Raise Water Level Near Right) - True - True +158105 - 0x04D18 (Flood Room 1) - 0x1C260 & 0x1831C - True +158106 - 0x01205 (Flood Room 2) - 0x04D18 & 0x1C260 & 0x1831C - True +158107 - 0x181AB (Flood Room 3) - 0x01205 & 0x1C260 & 0x1831C - True +158108 - 0x0117A (Flood Room 4) - 0x181AB & 0x1C260 & 0x1831C - True +158109 - 0x17ECA (Flood Room 5) - 0x0117A & 0x1C260 & 0x1831C - True +158110 - 0x18076 (Flood Room 6) - 0x17ECA & 0x1C260 & 0x1831C - True +Door - 0x0C316 (Elevator Room Entry) - 0x18076 +159034 - 0x337F8 (Flood Room EP) - 0x1C2DF - True + +Desert Elevator Room (Desert) - Desert Behind Elevator - 0x01317: +158111 - 0x17C31 (Elevator Room Transparent) - True - True +158113 - 0x012D7 (Elevator Room Hexagonal) - 0x17C31 & 0x0A015 - True +158114 - 0x0A015 (Elevator Room Hexagonal Control) - 0x17C31 - True +158115 - 0x0A15C (Elevator Room Bent 1) - True - True +158116 - 0x09FFF (Elevator Room Bent 2) - 0x0A15C - True +158117 - 0x0A15F (Elevator Room Bent 3) - 0x09FFF - True +159035 - 0x037BB (Elevator EP) - 0x01317 - True +Door - 0x01317 (Elevator) - 0x03608 + +Desert Behind Elevator (Desert): + +==Quarry== + +Quarry Obelisk (Quarry) - Entry - True: +159740 - 0xFFE40 (Obelisk Side 1) - 0x28A7B & 0x005F6 & 0x00859 & 0x17CB9 & 0x28A4A - True +159741 - 0xFFE41 (Obelisk Side 2) - 0x334B6 & 0x00614 & 0x0069D & 0x28A4C - True +159742 - 0xFFE42 (Obelisk Side 3) - 0x289CF & 0x289D1 - True +159743 - 0xFFE43 (Obelisk Side 4) - 0x33692 - True +159744 - 0xFFE44 (Obelisk Side 5) - 0x03E77 & 0x03E7C - True +159749 - 0x22073 (Obelisk) - True - True + +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F - Quarry Elevator - 0xFFD00 & 0xFFD01: +158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares & Dots +158603 - 0x17CF0 (Discard) - True - Arrows & Triangles +158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers +Laser - 0x01539 (Laser) - 0x03612 +Door - 0x09D6F (Entry 1) - 0x09E57 +159404 - 0x28A4A (Shore EP) - True - True +159410 - 0x334B6 (Entrance Pipe EP) - True - True +159412 - 0x28A4C (Sand Pile EP) - True - True +159420 - 0x289CF (Rock Line EP) - True - True +159421 - 0x289D1 (Rock Line Reflection EP) - True - True + +Quarry Elevator (Quarry) - Outside Quarry - 0x17CC4 - Quarry - 0x17CC4: +158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser +159403 - 0x17CB9 (Railroad EP) - 0x17CC4 - True + +Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07: +158119 - 0x17C09 (Entry 2 Panel) - True - Shapers & Eraser +Door - 0x17C07 (Entry 2) - 0x17C09 + +Quarry (Quarry) - Quarry Stoneworks Ground Floor - 0x02010: +159802 - 0xFFD01 (Inside Reached Independently) - True - True +158121 - 0x01E5A (Stoneworks Entry Left Panel) - True - Stars & Stars + Same Colored Symbol & Eraser +158122 - 0x01E59 (Stoneworks Entry Right Panel) - True - Triangles +Door - 0x02010 (Stoneworks Entry) - 0x01E59 & 0x01E5A + +Quarry Stoneworks Ground Floor (Quarry Stoneworks) - Quarry - 0x275FF - Quarry Stoneworks Middle Floor - 0x03678 - Outside Quarry - 0x17CE8 - Quarry Stoneworks Lift - TrueOneWay: +158123 - 0x275ED (Side Exit Panel) - True - True +Door - 0x275FF (Side Exit) - 0x275ED +158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser +158145 - 0x17CAC (Roof Exit Panel) - True - True +Door - 0x17CE8 (Roof Exit) - 0x17CAC + +Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - TrueOneWay: +158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser +158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser +158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser +158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser +158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots & Eraser +158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser + +Quarry Stoneworks Lift (Quarry Stoneworks) - Quarry Stoneworks Middle Floor - 0x03679 - Quarry Stoneworks Ground Floor - 0x03679 - Quarry Stoneworks Upper Floor - 0x03679: +158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser + +Quarry Stoneworks Upper Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - 0x03675 - Quarry Stoneworks Ground Floor - 0x0368A: +158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser +158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser +158134 - 0x00557 (Upper Row 1) - True - Colored Squares & Eraser +158135 - 0x005F1 (Upper Row 2) - 0x00557 - Colored Squares & Eraser +158136 - 0x00620 (Upper Row 3) - 0x005F1 - Colored Squares & Eraser +158137 - 0x009F5 (Upper Row 4) - 0x00620 - Colored Squares & Eraser +158138 - 0x0146C (Upper Row 5) - 0x009F5 - Stars & Stars + Same Colored Symbol & Eraser +158139 - 0x3C12D (Upper Row 6) - 0x0146C - Stars & Stars + Same Colored Symbol & Eraser +158140 - 0x03686 (Upper Row 7) - 0x3C12D - Stars & Stars + Same Colored Symbol & Eraser +158141 - 0x014E9 (Upper Row 8) - 0x03686 - Stars & Stars + Same Colored Symbol & Eraser +158142 - 0x03677 (Stairs Panel) - True - Colored Squares & Eraser +Door - 0x0368A (Stairs) - 0x03677 +158143 - 0x3C125 (Control Room Left) - 0x014E9 - Black/White Squares & Dots & Eraser & Symmetry +158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser & Stars & Stars + Same Colored Symbol +159411 - 0x0069D (Ramp EP) - 0x03676 & 0x275FF - True +159413 - 0x00614 (Lift EP) - 0x275FF & 0x03675 - True + +Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: +158146 - 0x034D4 (Intro Left) - True - Stars +158147 - 0x021D5 (Intro Right) - True - Shapers & Rotated Shapers & Triangles +158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers +158166 - 0x17CA6 (Boat Spawn) - True - Boat +Door - 0x2769B (Dock) - 0x17CA6 +Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 + +Quarry Boathouse Behind Staircase (Quarry Boathouse) - The Ocean - 0x17CA6: + +Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: +158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser +158150 - 0x021B4 (Front Row 2) - 0x021B3 - Shapers & Eraser +158151 - 0x021B0 (Front Row 3) - 0x021B4 - Shapers & Eraser +158152 - 0x021AF (Front Row 4) - 0x021B0 - Shapers & Eraser +158153 - 0x021AE (Front Row 5) - 0x021AF - Shapers & Eraser +Door - 0x17C50 (First Barrier) - 0x021AE + +Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - 0x03858: +158154 - 0x03858 (Ramp Horizontal Control) - True - Shapers & Eraser +159402 - 0x00859 (Moving Ramp EP) - 0x03858 & 0x03852 & 0x3865F - True + +Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F: +158155 - 0x38663 (Second Barrier Panel) - True - True +Door - 0x3865F (Second Barrier) - 0x38663 +158156 - 0x021B5 (Back First Row 1) - True - Shapers & Negative Shapers & Eraser +158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Shapers & Negative Shapers & Eraser +158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Shapers & Negative Shapers & Eraser +158159 - 0x021BB (Back First Row 4) - 0x021B7 - Shapers & Negative Shapers & Eraser +158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Shapers & Negative Shapers & Eraser +158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares +158162 - 0x3C124 (Back First Row 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares +158163 - 0x09DB3 (Back First Row 8) - 0x3C124 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares & Triangles +158164 - 0x09DB4 (Back First Row 9) - 0x09DB3 - Stars & Stars + Same Colored Symbol & Eraser & Colored Squares & Triangles +158165 - 0x275FA (Hook Control) - True - Shapers & Eraser +158167 - 0x0A3CB (Back Second Row 1) - 0x09DB4 - Black/White Squares & Colored Squares & Eraser & Shapers +158168 - 0x0A3CC (Back Second Row 2) - 0x0A3CB - Stars & Stars + Same Colored Symbol & Eraser & Shapers +158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Triangles & Eraser & Shapers +159401 - 0x005F6 (Hook EP) - 0x275FA & 0x03852 & 0x3865F - True + +==Shadows== + +Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 | 0x19665: +158170 - 0x334DB (Door Timer Outside) - True - True +Door - 0x19B24 (Timed Door) - 0x334DB | 0x334DC +158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True +158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True +158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True +158174 - 0x386FA (Far 1) - 0x0A8E0 - True +158175 - 0x1C33F (Far 2) - 0x386FA - True +158176 - 0x196E2 (Far 3) - 0x1C33F - True +158177 - 0x1972A (Far 4) - 0x196E2 - True +158178 - 0x19809 (Far 5) - 0x1972A - True +158179 - 0x19806 (Far 6) - 0x19809 - True +158180 - 0x196F8 (Far 7) - 0x19806 - True +158181 - 0x1972F (Far 8) - 0x196F8 - True +Door - 0x194B2 (Laser Entry Right) - 0x1972F +158182 - 0x19797 (Near 1) - 0x0A8E0 - True +158183 - 0x1979A (Near 2) - 0x19797 - True +158184 - 0x197E0 (Near 3) - 0x1979A - True +158185 - 0x197E8 (Near 4) - 0x197E0 - True +158186 - 0x197E5 (Near 5) - 0x197E8 - True +Door - 0x19665 (Laser Entry Left) - 0x197E5 +159400 - 0x28A7B (Quarry Stoneworks Rooftop Vent EP) - True - True + +Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: +158187 - 0x334DC (Door Timer Inside) - True - True +158188 - 0x198B5 (Intro 1) - True - True +158189 - 0x198BD (Intro 2) - 0x198B5 - True +158190 - 0x198BF (Intro 3) - 0x198BD & 0x19B24 - True +Door - 0x19865 (Quarry Barrier) - 0x198BF +Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF +158191 - 0x19771 (Intro 4) - 0x198BF - True +158192 - 0x0A8DC (Intro 5) - 0x19771 - True +Door - 0x1855B (Ledge Barrier) - 0x0A8DC +Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC + +Shadows Laser Room (Shadows): +158703 - 0x19650 (Laser Panel) - 0x194B2 & 0x19665 - True +Laser - 0x181B3 (Laser) - 0x19650 + +==Keep== + +Outside Keep (Keep) - Main Island - True: +159430 - 0x03E77 (Red Flowers EP) - True - True +159431 - 0x03E7C (Purple Flowers EP) - True - True + +Keep (Keep) - Outside Keep - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC: +158193 - 0x00139 (Hedge Maze 1) - True - True +158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True +158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Dots & Triangles +Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139 +Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA + +Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8: +Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139 +158194 - 0x019DC (Hedge Maze 2) - True - True +Door - 0x019D8 (Hedge Maze 2 Exit) - 0x019DC + +Keep 3rd Maze (Keep) - Keep - 0x019B5 - Keep 4th Maze - 0x019E6: +Door - 0x019B5 (Hedge Maze 3 Shortcut) - 0x019DC +158195 - 0x019E7 (Hedge Maze 3) - True - True +Door - 0x019E6 (Hedge Maze 3 Exit) - 0x019E7 + +Keep 4th Maze (Keep) - Keep - 0x0199A - Keep Tower - 0x01A0E: +Door - 0x0199A (Hedge Maze 4 Shortcut) - 0x019E7 +158196 - 0x01A0F (Hedge Maze 4) - True - True +Door - 0x01A0E (Hedge Maze 4 Exit) - 0x01A0F + +Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - True: +158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True +158200 - 0x01BE9 (Pressure Plates 2) - 0x0A3B9 - Stars & Stars + Same Colored Symbol & Triangles +Door - 0x01BEA (Pressure Plates 2 Exit) - 0x01BE9 + +Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5: +158201 - 0x0A3BB (Reset Pressure Plates 3) - True - True +158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Shapers & Stars +Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3 + +Keep 4th Pressure Plate (Keep) - Shadows - 0x09E3D - Keep Tower - 0x01D40: +158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True +158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Shapers & Rotated Shapers & Negative Shapers & Symmetry & Dots +Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F +158604 - 0x17D27 (Discard) - True - Arrows & Triangles +158205 - 0x09E49 (Shadows Shortcut Panel) - True - True +Door - 0x09E3D (Shadows Shortcut) - 0x09E49 + +Keep Tower (Keep) - Keep - 0x04F8F: +158206 - 0x0361B (Tower Shortcut Panel) - True - True +Door - 0x04F8F (Tower Shortcut) - 0x0361B +158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol +Laser - 0x014BB (Laser) - 0x0360E | 0x03317 +159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True +159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True +159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True +159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True +159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True +159250 - 0x28AE9 (Path EP) - True - True +159251 - 0x3348F (Hedges EP) - True - True + +==Shipwreck== + +Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True - Shipwreck Vault - 0x17BB4: +158654 - 0x00AFB (Vault Panel) - True - Symmetry & Sound Dots & Colored Dots +Door - 0x17BB4 (Vault Door) - 0x00AFB +158605 - 0x17D28 (Discard) - True - Arrows & Triangles +159220 - 0x03B22 (Circle Far EP) - True - True +159221 - 0x03B23 (Circle Left EP) - True - True +159222 - 0x03B24 (Circle Near EP) - True - True +159224 - 0x03A79 (Stern EP) - True - True +159225 - 0x28ABD (Rope Inner EP) - True - True +159226 - 0x28ABE (Rope Outer EP) - True - True +159230 - 0x3388F (Couch EP) - 0x17CDF | 0x0A054 - True + +Shipwreck Vault (Shipwreck): +158655 - 0x03535 (Vault Box) - True - True + +==Monastery== + +Monastery Obelisk (Monastery) - Entry - True: +159710 - 0xFFE10 (Obelisk Side 1) - 0x03ABC & 0x03ABE & 0x03AC0 & 0x03AC4 - True +159711 - 0xFFE11 (Obelisk Side 2) - 0x03AC5 - True +159712 - 0xFFE12 (Obelisk Side 3) - 0x03BE2 & 0x03BE3 & 0x0A409 - True +159713 - 0xFFE13 (Obelisk Side 4) - 0x006E5 & 0x006E6 & 0x006E7 & 0x034A7 & 0x034AD & 0x034AF & 0x03DAB & 0x03DAC & 0x03DAD - True +159714 - 0xFFE14 (Obelisk Side 5) - 0x03E01 - True +159715 - 0xFFE15 (Obelisk Side 6) - 0x289F4 & 0x289F5 - True +159719 - 0x00263 (Obelisk) - True - True + +Outside Monastery (Monastery) - Main Island - True - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: +158207 - 0x03713 (Laser Shortcut Panel) - True - True +Door - 0x0364E (Laser Shortcut) - 0x03713 +158208 - 0x00B10 (Entry Left) - True - True +158209 - 0x00C92 (Entry Right) - 0x00B10 - True +Door - 0x0C128 (Entry Inner) - 0x00B10 +Door - 0x0C153 (Entry Outer) - 0x00C92 +158210 - 0x00290 (Outside 1) - 0x09D9B - True +158211 - 0x00038 (Outside 2) - 0x09D9B & 0x00290 - True +158212 - 0x00037 (Outside 3) - 0x09D9B & 0x00038 - True +Door - 0x03750 (Garden Entry) - 0x00037 +158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True +Laser - 0x17C65 (Laser) - 0x17CA4 +159130 - 0x006E5 (Facade Left Near EP) - True - True +159131 - 0x006E6 (Facade Left Far Short EP) - True - True +159132 - 0x006E7 (Facade Left Far Long EP) - True - True +159136 - 0x03DAB (Facade Right Near EP) - True - True +159137 - 0x03DAC (Facade Left Stairs EP) - True - True +159138 - 0x03DAD (Facade Right Stairs EP) - True - True +159140 - 0x03E01 (Grass Stairs EP) - True - True +159120 - 0x03BE2 (Garden Left EP) - 0x03750 - True +159121 - 0x03BE3 (Garden Right EP) - True - True +159122 - 0x0A409 (Wall EP) - True - True + +Inside Monastery (Monastery): +158213 - 0x09D9B (Shutters Control) - True - Dots +158214 - 0x193A7 (Inside 1) - 0x00037 - True +158215 - 0x193AA (Inside 2) - 0x193A7 - True +158216 - 0x193AB (Inside 3) - 0x193AA - True +158217 - 0x193A6 (Inside 4) - 0x193AB - True +159133 - 0x034A7 (Left Shutter EP) - 0x09D9B - True +159134 - 0x034AD (Middle Shutter EP) - 0x09D9B - True +159135 - 0x034AF (Right Shutter EP) - 0x09D9B - True + +Monastery Garden (Monastery): + +==Town== + +Town Obelisk (Town) - Entry - True: +159750 - 0xFFE50 (Obelisk Side 1) - 0x035C7 - True +159751 - 0xFFE51 (Obelisk Side 2) - 0x01848 & 0x03D06 & 0x33530 & 0x33600 & 0x28A2F & 0x28A37 & 0x334A3 & 0x3352F - True +159752 - 0xFFE52 (Obelisk Side 3) - 0x33857 & 0x33879 & 0x03C19 - True +159753 - 0xFFE53 (Obelisk Side 4) - 0x28B30 & 0x035C9 - True +159754 - 0xFFE54 (Obelisk Side 5) - 0x03335 & 0x03412 & 0x038A6 & 0x038AA & 0x03E3F & 0x03E40 & 0x28B8E - True +159755 - 0xFFE55 (Obelisk Side 6) - 0x28B91 & 0x03BCE & 0x03BCF & 0x03BD1 & 0x339B6 & 0x33A20 & 0x33A29 & 0x33A2A & 0x33B06 - True +159759 - 0x0A16C (Obelisk) - True - True + +Town (Town) - Main Island - True - The Ocean - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - Town RGB House - 0x28A61 - Town Inside Cargo Box - 0x0A0C9 - Outside Windmill - True: +158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat +158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Triangles +Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 +158707 - 0x09F98 (Desert Laser Redirect Control) - True - True +158220 - 0x18590 (Transparent) - True - Symmetry +158221 - 0x28AE3 (Vines) - 0x18590 - True +158222 - 0x28938 (Apple Tree) - 0x28AE3 - True +158223 - 0x079DF (Triple Exit) - 0x28938 - True +158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots & Full Dots & Colored Squares +158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Rotated Shapers & Dots & Full Dots & Stars +158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Negative Shapers & Dots & Full Dots +158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots & Full Dots +158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots & Full Dots & Triangles +Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 +158225 - 0x28998 (RGB House Entry Panel) - True - Stars & Rotated Shapers +Door - 0x28A61 (RGB House Entry) - 0x28998 +158226 - 0x28A0D (Church Entry Panel) - 0x28A61 - Stars +Door - 0x03BB0 (Church Entry) - 0x28A0D +158228 - 0x28A79 (Maze Panel) - True - True +Door - 0x28AA2 (Maze Stairs) - 0x28A79 +159540 - 0x03335 (Tower Underside Third EP) - True - True +159541 - 0x03412 (Tower Underside Fourth EP) - True - True +159542 - 0x038A6 (Tower Underside First EP) - True - True +159543 - 0x038AA (Tower Underside Second EP) - True - True +159545 - 0x03E40 (RGB House Green EP) - 0x334D8 - True +159546 - 0x28B8E (Maze Bridge Underside EP) - 0x2896A - True +159552 - 0x03BCF (Black Line Redirect EP) - True - True +159800 - 0xFFF80 (Pet the Dog) - True - True + +Town Inside Cargo Box (Town): +158606 - 0x17D01 (Cargo Box Discard) - True - Arrows & Triangles + +Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A: +158229 - 0x2896A (Maze Rooftop Bridge Control) - True - Shapers +159544 - 0x03E3F (RGB House Red EP) - 0x334D8 - True + +Town Red Rooftop (Town): +158607 - 0x17C71 (Rooftop Discard) - True - Arrows & Triangles +158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Dots +158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Black/White Squares +158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Stars +158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Shapers +158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Triangles +158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True + +Town Wooden Rooftop (Town): +158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots + +Town Church (Town): +158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True +159553 - 0x03BD1 (Black Line Church EP) - True - True + +Town RGB House (Town RGB House) - Town RGB House Upstairs - 0x2897B: +158242 - 0x034E4 (Sound Room Left) - True - True +158243 - 0x034E3 (Sound Room Right) - True - Sound Dots +Door - 0x2897B (Stairs) - 0x034E4 & 0x034E3 + +Town RGB House Upstairs (Town RGB House Upstairs): +158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & Colored Squares +158245 - 0x03C0C (Left) - 0x334D8 - Stars +158246 - 0x03C08 (Right) - 0x334D8 - Dots & Symmetry & Colored Dots + +Town Tower Bottom (Town Tower) - Town - True - Town Tower After First Door - 0x27799: +Door - 0x27799 (First Door) - 0x28A69 + +Town Tower After First Door (Town Tower) - Town Tower After Second Door - 0x27798: +Door - 0x27798 (Second Door) - 0x28ACC + +Town Tower After Second Door (Town Tower) - Town Tower After Third Door - 0x2779C: +Door - 0x2779C (Third Door) - 0x28AD9 + +Town Tower After Third Door (Town Tower) - Town Tower Top - 0x2779A: +Door - 0x2779A (Fourth Door) - 0x28B39 + +Town Tower Top (Town): +158708 - 0x032F5 (Laser Panel) - True - True +Laser - 0x032F9 (Laser) - 0x032F5 +159422 - 0x33692 (Brown Bridge EP) - True - True +159551 - 0x03BCE (Black Line Tower EP) - True - True + +==Windmill & Theater== + +Outside Windmill (Windmill) - Windmill Interior - 0x1845B: +159010 - 0x037B6 (First Blade EP) - 0x17D02 - True +159011 - 0x037B2 (Second Blade EP) - 0x17D02 - True +159012 - 0x000F7 (Third Blade EP) - 0x17D02 - True +158241 - 0x17F5F (Entry Panel) - True - Dots +Door - 0x1845B (Entry) - 0x17F5F + +Windmill Interior (Windmill) - Theater - 0x17F88: +158247 - 0x17D02 (Turn Control) - True - Dots +158248 - 0x17F89 (Theater Entry Panel) - True - Black/White Squares & Triangles +Door - 0x17F88 (Theater Entry) - 0x17F89 + +Theater (Theater) - Town - 0x0A16D | 0x3CCDF: +158656 - 0x00815 (Video Input) - True - True +158657 - 0x03553 (Tutorial Video) - 0x00815 & 0x03481 - True +158658 - 0x03552 (Desert Video) - 0x00815 & 0x0339E - True +158659 - 0x0354E (Jungle Video) - 0x00815 & 0x03702 - True +158660 - 0x03549 (Challenge Video) - 0x00815 & 0x0356B - True +158661 - 0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True +158662 - 0x03545 (Mountain Video) - 0x00815 & 0x03542 - True +158249 - 0x0A168 (Exit Left Panel) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Shapers +158250 - 0x33AB2 (Exit Right Panel) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Shapers +Door - 0x0A16D (Exit Left) - 0x0A168 +Door - 0x3CCDF (Exit Right) - 0x33AB2 +158608 - 0x17CF7 (Discard) - True - Arrows & Triangles +159554 - 0x339B6 (Eclipse EP) - 0x03549 & 0x0A16D & 0x3CCDF - True +159555 - 0x33A29 (Window EP) - 0x03553 - True +159556 - 0x33A2A (Door EP) - 0x03553 - True +159558 - 0x33B06 (Church EP) - 0x0354E - True + +==Jungle== + +Jungle (Jungle) - Main Island - True - The Ocean - 0x17CDF: +158251 - 0x17CDF (Shore Boat Spawn) - True - Boat +158609 - 0x17F9B (Discard) - True - Arrows & Triangles +158252 - 0x002C4 (First Row 1) - True - True +158253 - 0x00767 (First Row 2) - 0x002C4 - True +158254 - 0x002C6 (First Row 3) - 0x00767 - True +158255 - 0x0070E (Second Row 1) - 0x002C6 - True +158256 - 0x0070F (Second Row 2) - 0x0070E - True +158257 - 0x0087D (Second Row 3) - 0x0070F - True +158258 - 0x002C7 (Second Row 4) - 0x0087D - True +158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True +Door - 0x1475B (Popup Wall) - 0x17CAB +158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound Dots +158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound Dots +158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound Dots +158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound Dots +158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound Dots +158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound Dots +158709 - 0x03616 (Laser Panel) - 0x014B2 - True +Laser - 0x00274 (Laser) - 0x03616 +158266 - 0x337FA (Laser Shortcut Panel) - True - True +Door - 0x3873B (Laser Shortcut) - 0x337FA +159100 - 0x03ABC (Long Arch Moss EP) - True - True +159101 - 0x03ABE (Straight Left Moss EP) - True - True +159102 - 0x03AC0 (Pop-up Wall Moss EP) - True - True +159103 - 0x03AC4 (Short Arch Moss EP) - True - True +159150 - 0x289F4 (Entrance EP) - True - True +159151 - 0x289F5 (Tree Halo EP) - True - True +159350 - 0x035CB (Bamboo CCW EP) - True - True +159351 - 0x035CF (Bamboo CW EP) - True - True + +Outside Jungle River (Jungle) - Main Island - True - Monastery Garden - 0x0CF2A - Jungle Vault - 0x15287: +158267 - 0x17CAA (Monastery Garden Shortcut Panel) - True - True +Door - 0x0CF2A (Monastery Garden Shortcut) - 0x17CAA +158663 - 0x15ADD (Vault Panel) - True - Black/White Squares & Dots +Door - 0x15287 (Vault Door) - 0x15ADD +159110 - 0x03AC5 (Green Leaf Moss EP) - True - True + +Jungle Vault (Jungle): +158664 - 0x03702 (Vault Box) - True - True + +==Bunker== + +Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: +158268 - 0x17C2E (Entry Panel) - True - Black/White Squares +Door - 0x0C2A4 (Entry) - 0x17C2E + +Bunker (Bunker) - Bunker Glass Room - 0x17C79: +158269 - 0x09F7D (Intro Left 1) - True - Colored Squares +158270 - 0x09FDC (Intro Left 2) - 0x09F7D - Colored Squares & Black/White Squares +158271 - 0x09FF7 (Intro Left 3) - 0x09FDC - Colored Squares & Black/White Squares +158272 - 0x09F82 (Intro Left 4) - 0x09FF7 - Colored Squares & Black/White Squares +158273 - 0x09FF8 (Intro Left 5) - 0x09F82 - Colored Squares & Black/White Squares +158274 - 0x09D9F (Intro Back 1) - 0x09FF8 - Colored Squares & Black/White Squares +158275 - 0x09DA1 (Intro Back 2) - 0x09D9F - Colored Squares +158276 - 0x09DA2 (Intro Back 3) - 0x09DA1 - Colored Squares +158277 - 0x09DAF (Intro Back 4) - 0x09DA2 - Colored Squares +158278 - 0x0A099 (Tinted Glass Door Panel) - 0x09DAF - True +Door - 0x17C79 (Tinted Glass Door) - 0x0A099 + +Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: +158279 - 0x0A010 (Glass Room 1) - 0x17C79 - Colored Squares +158280 - 0x0A01B (Glass Room 2) - 0x17C79 & 0x0A010 - Colored Squares & Black/White Squares +158281 - 0x0A01F (Glass Room 3) - 0x17C79 & 0x0A01B - Colored Squares & Black/White Squares +Door - 0x0C2A3 (UV Room Entry) - 0x0A01F + +Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: +158282 - 0x34BC5 (Drop-Down Door Open) - True - True +158283 - 0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True +158284 - 0x17E63 (UV Room 1) - 0x34BC5 - Colored Squares +158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Colored Squares & Black/White Squares +Door - 0x0A08D (Elevator Room Entry) - 0x17E67 + +Bunker Elevator Section (Bunker) - Bunker Elevator - TrueOneWay: +159311 - 0x035F5 (Tinted Door EP) - 0x17C79 - True + +Bunker Elevator (Bunker) - Bunker Elevator Section - 0x0A079 - Bunker Cyan Room - 0x0A079 - Bunker Green Room - 0x0A079 - Bunker Laser Platform - 0x0A079 - Outside Bunker - 0x0A079: +158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares + +Bunker Cyan Room (Bunker) - Bunker Elevator - TrueOneWay: + +Bunker Green Room (Bunker) - Bunker Elevator - TrueOneWay: +159310 - 0x000D3 (Green Room Flowers EP) - True - True + +Bunker Laser Platform (Bunker) - Bunker Elevator - TrueOneWay: +158710 - 0x09DE0 (Laser Panel) - True - True +Laser - 0x0C2B2 (Laser) - 0x09DE0 + +==Swamp== + +Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True: +158287 - 0x0056E (Entry Panel) - True - Shapers & Black/White Squares +Door - 0x00C1C (Entry) - 0x0056E +159321 - 0x03603 (Purple Sand Middle EP) - 0x17E2B - True +159322 - 0x03601 (Purple Sand Top EP) - 0x17E2B - True +159327 - 0x035DE (Purple Sand Bottom EP) - True - True + +Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: +158288 - 0x00469 (Intro Front 1) - True - Shapers +158289 - 0x00472 (Intro Front 2) - 0x00469 - Shapers +158290 - 0x00262 (Intro Front 3) - 0x00472 - Shapers +158291 - 0x00474 (Intro Front 4) - 0x00262 - Shapers +158292 - 0x00553 (Intro Front 5) - 0x00474 - Shapers +158293 - 0x0056F (Intro Front 6) - 0x00553 - Shapers +158294 - 0x00390 (Intro Back 1) - 0x0056F - Shapers & Black/White Squares +158295 - 0x010CA (Intro Back 2) - 0x00390 - Shapers & Black/White Squares +158296 - 0x00983 (Intro Back 3) - 0x010CA - Shapers & Rotated Shapers & Black/White Squares +158297 - 0x00984 (Intro Back 4) - 0x00983 - Shapers & Rotated Shapers & Black/White Squares +158298 - 0x00986 (Intro Back 5) - 0x00984 - Shapers & Triangles +158299 - 0x00985 (Intro Back 6) - 0x00986 - Shapers & Triangles +158300 - 0x00987 (Intro Back 7) - 0x00985 - Rotated Shapers & Triangles +158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers & Triangles + +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +158302 - 0x00609 (Sliding Bridge) - True - Shapers +159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True +159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True + +Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +158313 - 0x00982 (Platform Row 1) - True - Shapers +158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers +158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers +158316 - 0x00990 (Platform Row 4) - 0x0098F - Shapers +Door - 0x184B7 (Between Bridges First Door) - 0x00990 +158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers +158318 - 0x17C0E (Platform Shortcut Right Panel) - 0x17C0D - Shapers +Door - 0x38AE6 (Platform Shortcut) - 0x17C0E +Door - 0x04B7F (Cyan Water Pump) - 0x00006 + +Swamp Cyan Underwater (Swamp): +158307 - 0x00002 (Cyan Underwater 1) - True - Shapers & Negative Shapers & Black/White Squares +158308 - 0x00004 (Cyan Underwater 2) - 0x00002 - Shapers & Negative Shapers & Black/White Squares +158309 - 0x00005 (Cyan Underwater 3) - 0x00004 - Shapers & Negative Shapers & Stars +158310 - 0x013E6 (Cyan Underwater 4) - 0x00005 - Shapers & Negative Shapers & Stars +158311 - 0x00596 (Cyan Underwater 5) - 0x013E6 - Shapers & Negative Shapers & Dots +158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers +159340 - 0x03AA6 (Cyan Underwater Sliding Bridge EP) - 0x18488 - True + +Swamp Between Bridges Near (Swamp) - Swamp Between Bridges Far - 0x18507: +158303 - 0x00999 (Between Bridges Near Row 1) - 0x00990 - Shapers +158304 - 0x0099D (Between Bridges Near Row 2) - 0x00999 - Shapers +158305 - 0x009A0 (Between Bridges Near Row 3) - 0x0099D - Shapers +158306 - 0x009A1 (Between Bridges Near Row 4) - 0x009A0 - Shapers +Door - 0x18507 (Between Bridges Second Door) - 0x009A1 + +Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay: +158319 - 0x00007 (Between Bridges Far Row 1) - 0x009A1 - Rotated Shapers & Dots +158320 - 0x00008 (Between Bridges Far Row 2) - 0x00007 - Rotated Shapers & Dots +158321 - 0x00009 (Between Bridges Far Row 3) - 0x00008 - Rotated Shapers & Dots +158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers & Dots +Door - 0x183F2 (Red Water Pump) - 0x00596 + +Swamp Red Underwater (Swamp) - Swamp Maze - 0x305D5: +158323 - 0x00001 (Red Underwater 1) - True - Symmetry & Shapers & Negative Shapers +158324 - 0x014D2 (Red Underwater 2) - True - Symmetry & Shapers & Negative Shapers +158325 - 0x014D4 (Red Underwater 3) - True - Symmetry & Shapers & Negative Shapers & Eraser +158326 - 0x014D1 (Red Underwater 4) - True - Symmetry & Shapers & Negative Shapers & Eraser +Door - 0x305D5 (Red Underwater Exit) - 0x014D1 + +Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5: +158327 - 0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers +159331 - 0x016B2 (Rotating Bridge CCW EP) - 0x181F5 - True +159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True + +Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +158328 - 0x09DB8 (Boat Spawn) - True - Boat +158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers & Dots +158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Dots +158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Dots +158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers & Dots +Door - 0x18482 (Blue Water Pump) - 0x00E3A +159332 - 0x3365F (Boat EP) - 0x09DB8 - True +159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True + +Swamp Long Bridge (Swamp) - Swamp Near Boat - 0x17E2B - Outside Swamp - 0x17E2B: +158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers + +Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Underwater - 0x0A1D6 - Swamp Near Boat - TrueOneWay: +Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A + +Swamp Purple Underwater (Swamp): +158333 - 0x009A6 (Purple Underwater) - True - Shapers & Dots +159330 - 0x03A9E (Purple Underwater Right EP) - True - True +159336 - 0x03A93 (Purple Underwater Left EP) - True - True + +Swamp Blue Underwater (Swamp): +158334 - 0x009AB (Blue Underwater 1) - True - Shapers & Negative Shapers +158335 - 0x009AD (Blue Underwater 2) - 0x009AB - Shapers & Negative Shapers +158336 - 0x009AE (Blue Underwater 3) - 0x009AD - Shapers & Negative Shapers +158337 - 0x009AF (Blue Underwater 4) - 0x009AE - Shapers & Negative Shapers +158338 - 0x00006 (Blue Underwater 5) - 0x009AF - Shapers & Negative Shapers + +Swamp Maze (Swamp) - Swamp Laser Area - 0x17C0A & 0x17E07: +158340 - 0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers +158112 - 0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers + +Swamp Laser Area (Swamp) - Outside Swamp - 0x2D880: +158711 - 0x03615 (Laser Panel) - True - True +Laser - 0x00BF6 (Laser) - 0x03615 +158341 - 0x17C05 (Laser Shortcut Left Panel) - True - Shapers & Colored Squares & Stars & Stars + Same Colored Symbol +158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Colored Squares & Stars & Stars + Same Colored Symbol +Door - 0x2D880 (Laser Shortcut) - 0x17C02 + +==Treehouse== + +Treehouse Obelisk (Treehouse) - Entry - True: +159720 - 0xFFE20 (Obelisk Side 1) - 0x0053D & 0x0053E & 0x00769 - True +159721 - 0xFFE21 (Obelisk Side 2) - 0x33721 & 0x220A7 & 0x220BD - True +159722 - 0xFFE22 (Obelisk Side 3) - 0x03B22 & 0x03B23 & 0x03B24 & 0x03B25 & 0x03A79 & 0x28ABD & 0x28ABE - True +159723 - 0xFFE23 (Obelisk Side 4) - 0x3388F & 0x28B29 & 0x28B2A - True +159724 - 0xFFE24 (Obelisk Side 5) - 0x018B6 & 0x033BE & 0x033BF & 0x033DD & 0x033E5 - True +159725 - 0xFFE25 (Obelisk Side 6) - 0x28AE9 & 0x3348F - True +159729 - 0x00097 (Obelisk) - True - True + +Treehouse Beach (Treehouse Beach) - Main Island - True: +159200 - 0x0053D (Rock Shadow EP) - True - True +159201 - 0x0053E (Sand Shadow EP) - True - True +159212 - 0x220BD (Both Orange Bridges EP) - 0x17DA2 & 0x17DDB - True + +Treehouse Entry Area (Treehouse) - Treehouse Between Entry Doors - 0x0C309 - The Ocean - 0x17C95: +158343 - 0x17C95 (Boat Spawn) - True - Boat +158344 - 0x0288C (First Door Panel) - True - Stars +Door - 0x0C309 (First Door) - 0x0288C +159210 - 0x33721 (Buoy EP) - 0x17C95 - True + +Treehouse Between Entry Doors (Treehouse) - Treehouse Yellow Bridge - 0x0C310: +158345 - 0x02886 (Second Door Panel) - True - Stars & Stars + Same Colored Symbol & Triangles +Door - 0x0C310 (Second Door) - 0x02886 + +Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4: +158346 - 0x17D72 (Yellow Bridge 1) - True - Stars +158347 - 0x17D8F (Yellow Bridge 2) - 0x17D72 - Stars +158348 - 0x17D74 (Yellow Bridge 3) - 0x17D8F - Stars +158349 - 0x17DAC (Yellow Bridge 4) - 0x17D74 - Stars +158350 - 0x17D9E (Yellow Bridge 5) - 0x17DAC - Stars +158351 - 0x17DB9 (Yellow Bridge 6) - 0x17D9E - Stars +158352 - 0x17D9C (Yellow Bridge 7) - 0x17DB9 - Stars +158353 - 0x17DC2 (Yellow Bridge 8) - 0x17D9C - Stars +158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars + +Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: +158355 - 0x0A182 (Third Door Panel) - True - Stars +Door - 0x0A181 (Third Door) - 0x0A182 + +Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True: +158356 - 0x2700B (Laser House Door Timer Outside) - True - True + +Treehouse First Purple Bridge (Treehouse) - Treehouse Second Purple Bridge - 0x17D6C: +158357 - 0x17DC8 (First Purple Bridge 1) - True - Stars & Dots & Triangles +158358 - 0x17DC7 (First Purple Bridge 2) - 0x17DC8 - Stars & Dots & Triangles +158359 - 0x17CE4 (First Purple Bridge 3) - 0x17DC7 - Stars & Dots & Triangles +158360 - 0x17D2D (First Purple Bridge 4) - 0x17CE4 - Stars & Dots & Triangles +158361 - 0x17D6C (First Purple Bridge 5) - 0x17D2D - Stars & Dots & Triangles + +Treehouse Right Orange Bridge (Treehouse) - Treehouse Drawbridge Platform - 0x17DA2: +158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Colored Squares +158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars & Stars + Same Colored Symbol & Colored Squares +158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars & Stars + Same Colored Symbol & Colored Squares +158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars & Stars + Same Colored Symbol & Colored Squares +158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars & Stars + Same Colored Symbol & Colored Squares +158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars & Stars + Same Colored Symbol & Colored Squares +158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars & Stars + Same Colored Symbol & Colored Squares +158398 - 0x17DCA (Right Orange Bridge 8) - 0x17DCC - Stars & Stars + Same Colored Symbol & Colored Squares +158399 - 0x17D8E (Right Orange Bridge 9) - 0x17DCA - Stars & Stars + Same Colored Symbol & Colored Squares +158400 - 0x17DB7 (Right Orange Bridge 10 & Directional) - 0x17D8E - Stars & Stars + Same Colored Symbol & Colored Squares +158401 - 0x17DB1 (Right Orange Bridge 11) - 0x17DB7 - Stars & Stars + Same Colored Symbol & Colored Squares +158402 - 0x17DA2 (Right Orange Bridge 12) - 0x17DB1 - Stars & Stars + Same Colored Symbol & Colored Squares + +Treehouse Drawbridge Platform (Treehouse) - Main Island - 0x0C32D: +158404 - 0x037FF (Drawbridge Panel) - True - Stars +Door - 0x0C32D (Drawbridge) - 0x037FF + +Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: +158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares +158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares +158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares +158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares +158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Stars + Same Colored Symbol & Colored Squares +158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Stars + Same Colored Symbol & Black/White Squares & Colored Squares +158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Stars + Same Colored Symbol & Colored Squares + +Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDB - Treehouse Laser Room Back Platform - 0x17DDB - Treehouse Burned House - 0x17DDB: +158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Stars + Same Colored Symbol & Triangles +158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Stars + Same Colored Symbol & Triangles +158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Stars + Same Colored Symbol & Triangles +158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Stars + Same Colored Symbol & Triangles +158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers +158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers +158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers +158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers +158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Black/White Squares & Stars + Same Colored Symbol +158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Stars + Same Colored Symbol & Shapers +158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Stars + Same Colored Symbol & Shapers +158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers +158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Stars + Same Colored Symbol & Shapers & Triangles +158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers +158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Stars + Same Colored Symbol & Shapers & Triangles + +Treehouse Green Bridge (Treehouse) - Treehouse Green Bridge Front House - 0x17E61 - Treehouse Green Bridge Left House - 0x17E61: +158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers & Negative Shapers & Stars + Same Colored Symbol +158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol +158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol + +Treehouse Green Bridge Front House (Treehouse): +158610 - 0x17FA9 (Green Bridge Discard) - True - Arrows & Triangles + +Treehouse Green Bridge Left House (Treehouse): +159211 - 0x220A7 (Right Orange Bridge EP) - 0x17DA2 - True + +Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB | 0x17CBC + +Treehouse Laser Room Back Platform (Treehouse): +158611 - 0x17FA0 (Laser Discard) - True - Arrows & Triangles + +Treehouse Burned House (Treehouse): +159202 - 0x00769 (Burned House Beach EP) - True - True + +Treehouse Laser Room (Treehouse): +158712 - 0x03613 (Laser Panel) - True - True +158403 - 0x17CBC (Laser House Door Timer Inside) - True - True +Laser - 0x028A4 (Laser) - 0x03613 + +==Mountain (Outside)== + +Mountainside Obelisk (Mountainside) - Entry - True: +159730 - 0xFFE30 (Obelisk Side 1) - 0x001A3 & 0x335AE - True +159731 - 0xFFE31 (Obelisk Side 2) - 0x000D3 & 0x035F5 & 0x09D5D & 0x09D5E & 0x09D63 - True +159732 - 0xFFE32 (Obelisk Side 3) - 0x3370E & 0x035DE & 0x03601 & 0x03603 & 0x03D0D & 0x3369A & 0x336C8 & 0x33505 - True +159733 - 0xFFE33 (Obelisk Side 4) - 0x03A9E & 0x016B2 & 0x3365F & 0x03731 & 0x036CE & 0x03C07 & 0x03A93 - True +159734 - 0xFFE34 (Obelisk Side 5) - 0x03AA6 & 0x3397C & 0x0105D & 0x0A304 - True +159735 - 0xFFE35 (Obelisk Side 6) - 0x035CB & 0x035CF - True +159739 - 0x00367 (Obelisk) - True - True + +Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: +159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True +158612 - 0x17C42 (Discard) - True - Arrows & Triangles +158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Triangles +Door - 0x00085 (Vault Door) - 0x002A6 +159301 - 0x335AE (Cloud Cycle EP) - True - True +159325 - 0x33505 (Bush EP) - True - True +159335 - 0x03C07 (Apparent River EP) - True - True + +Mountainside Vault (Mountainside): +158666 - 0x03542 (Vault Box) - True - True + +Mountaintop (Mountaintop) - Mountain Floor 1 - 0x17C34: +158405 - 0x0042D (River Shape) - True - True +158406 - 0x09F7F (Box Short) - 7 Lasers + Redirect - True +158407 - 0x17C34 (Mountain Entry Panel) - 0x09F7F - Triangles +158800 - 0xFFF00 (Box Long) - 11 Lasers + Redirect & 0x17C34 - True +159300 - 0x001A3 (River Shape EP) - True - True +159320 - 0x3370E (Arch Black EP) - True - True +159324 - 0x336C8 (Arch White Right EP) - True - True +159326 - 0x3369A (Arch White Left EP) - True - True + +==Mountain (Inside)== + +Mountain Floor 1 (Mountain Floor 1) - Mountain Floor 1 Bridge - 0x09E39: +158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser + +Mountain Floor 1 Bridge (Mountain Floor 1) - Mountain Floor 1 At Door - TrueOneWay: +158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots +158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots & Stars & Stars + Same Colored Symbol +158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Stars & Stars + Same Colored Symbol +158412 - 0x09E69 (Right Row 4) - 0x09E72 - Black/White Squares & Eraser & Stars & Stars + Same Colored Symbol +158413 - 0x09E7B (Right Row 5) - 0x09E69 - Dots & Full Dots & Triangles +158414 - 0x09E73 (Left Row 1) - True - Dots & Black/White Squares +158415 - 0x09E75 (Left Row 2) - 0x09E73 - Arrows & Black/White Squares +158416 - 0x09E78 (Left Row 3) - 0x09E75 - Arrows & Stars +158417 - 0x09E79 (Left Row 4) - 0x09E78 - Arrows & Shapers & Rotated Shapers +158418 - 0x09E6C (Left Row 5) - 0x09E79 - Arrows & Black/White Squares & Stars & Stars + Same Colored Symbol +158419 - 0x09E6F (Left Row 6) - 0x09E6C - Arrows & Dots & Full Dots +158420 - 0x09E6B (Left Row 7) - 0x09E6F - Arrows & Dots & Full Dots +158421 - 0x33AF5 (Back Row 1) - True - Symmetry & Triangles +158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Triangles +158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Triangles +158424 - 0x09EAD (Trash Pillar 1) - True - Triangles & Arrows +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Triangles & Arrows + +Mountain Floor 1 At Door (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B + +Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Beyond Bridge - 0x09E86 - Mountain Floor 2 Above The Abyss - True - Mountain Pink Bridge EP - TrueOneWay: +158426 - 0x09FD3 (Near Row 1) - True - Stars & Stars + Same Colored Symbol & Colored Squares +158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Stars + Same Colored Symbol & Triangles +158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Stars + Same Colored Symbol & Colored Squares & Eraser +158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Stars + Same Colored Symbol & Shapers & Eraser +158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Symmetry & Triangles +Door - 0x09FFB (Staircase Near) - 0x09FD8 + +Mountain Floor 2 Above The Abyss (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD & 0x09ED8 & 0x09E86: +Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 + +Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): +158431 - 0x09E86 (Light Bridge Controller Near) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser + +Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07 - Mountain Pink Bridge EP - TrueOneWay - Mountain Floor 2 - 0x09ED8: +158432 - 0x09FCC (Far Row 1) - True - Black/White Squares +158433 - 0x09FCE (Far Row 2) - 0x09FCC - Triangles +158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars +158435 - 0x09FD0 (Far Row 4) - 0x09FCF - Stars & Stars + Same Colored Symbol & Colored Squares +158436 - 0x09FD1 (Far Row 5) - 0x09FD0 - Dots +158437 - 0x09FD2 (Far Row 6) - 0x09FD1 - Shapers +Door - 0x09E07 (Staircase Far) - 0x09FD2 + +Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2): +158438 - 0x09ED8 (Light Bridge Controller Far) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser + +Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay: +158613 - 0x17F93 (Elevator Discard) - True - Arrows & Triangles + +Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Floor 3 - 0x09EEB: +158439 - 0x09EEB (Elevator Control Panel) - True - Dots + +Mountain Floor 3 (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89 - Mountain Pink Bridge EP - TrueOneWay: +158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser +158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser +158442 - 0x09F01 (Giant Puzzle Top Right) - True - Shapers & Eraser +158443 - 0x09EFF (Giant Puzzle Top Left) - True - Rotated Shapers +158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry +159313 - 0x09D5D (Yellow Bridge EP) - 0x09E86 & 0x09ED8 - True +159314 - 0x09D5E (Blue Bridge EP) - 0x09E86 & 0x09ED8 - True +Door - 0x09F89 (Exit) - 0x09FDA + +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Path to Caves - 0x17F33 - Mountain Bottom Floor Pillars Room - 0x0C141: +158614 - 0x17FA2 (Discard) - 0xFFF00 - Arrows & Triangles +158445 - 0x01983 (Pillars Room Entry Left) - True - Shapers & Stars +158446 - 0x01987 (Pillars Room Entry Right) - True - Colored Squares & Dots +Door - 0x0C141 (Pillars Room Entry) - 0x01983 & 0x01987 +Door - 0x17F33 (Rock Open) - 0x17FA2 | 0x334E1 + +Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB & 0x33961: +158522 - 0x0383A (Right Pillar 1) - True - Stars +158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Full Dots & Triangles +158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry & Triangles +158526 - 0x0383D (Left Pillar 1) - True - Triangles +158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares & Stars & Stars + Same Colored Symbol +158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers +158529 - 0x339BB (Left Pillar 4) - 0x03859 - Triangles & Symmetry + +Elevator (Mountain Bottom Floor): +158530 - 0x3D9A6 (Elevator Door Close Left) - True - True +158531 - 0x3D9A7 (Elevator Door Close Right) - True - True +158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True +158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True +158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True +158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True +158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA & 7 Lasers | 0x3D9A8 & 7 Lasers - True + +Mountain Pink Bridge EP (Mountain Floor 2): +159312 - 0x09D63 (Pink Bridge EP) - 0x09E39 - True + +Mountain Path to Caves (Mountain Bottom Floor) - Caves - 0x2D77D: +158447 - 0x00FF8 (Caves Entry Panel) - True - Black/White Squares & Arrows & Triangles +Door - 0x2D77D (Caves Entry) - 0x00FF8 +158448 - 0x334E1 (Rock Control) - True - True + +==Caves== + +Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Caves Path to Challenge - 0x019A5: +158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares +158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares +158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots +158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Full Dots & Triangles & Arrows +158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Full Dots & Triangles & Arrows +158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Full Dots & Triangles & Arrows +158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Full Dots & Triangles & Arrows +158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Symmetry & Triangles & Arrows +158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Triangles & Colored Squares & Arrows +158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Triangles & Colored Squares & Arrows +158461 - 0x0097B (Blue Tunnel Left Second 3) - 0x00973 - Triangles & Colored Squares & Arrows & Stars & Stars + Same Colored Symbol +158462 - 0x0097D (Blue Tunnel Left Second 4) - 0x0097B - Triangles & Colored Squares & Arrows & Stars & Stars + Same Colored Symbol +158463 - 0x0097E (Blue Tunnel Left Second 5) - 0x0097D - Triangles & Colored Squares & Arrows & Stars & Stars + Same Colored Symbol +158464 - 0x00994 (Blue Tunnel Right Second 1) - True - Rotated Shapers & Triangles +158465 - 0x334D5 (Blue Tunnel Right Second 2) - 0x00994 - Rotated Shapers & Triangles +158466 - 0x00995 (Blue Tunnel Right Second 3) - 0x334D5 - Rotated Shapers & Triangles +158467 - 0x00996 (Blue Tunnel Right Second 4) - 0x00995 - Rotated Shapers & Triangles +158468 - 0x00998 (Blue Tunnel Right Second 5) - 0x00996 - Shapers & Triangles +158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Shapers & Triangles +158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Shapers & Symmetry & Eraser +158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Shapers & Negative Shapers & Triangles +158472 - 0x32962 (First Floor Left) - True - Rotated Shapers & Dots +158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares & Stars + Same Colored Symbol & Shapers & Triangles +158474 - 0x01A31 (First Floor Middle) - True - Colored Squares +158475 - 0x00B71 (First Floor Right) - True - Colored Squares & Stars & Stars + Same Colored Symbol & Eraser & Shapers & Negative Shapers & Dots +158478 - 0x288EA (First Wooden Beam) - True - Colored Squares & Black/White Squares & Eraser +158479 - 0x288FC (Second Wooden Beam) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Eraser +158480 - 0x289E7 (Third Wooden Beam) - True - Black/White Squares & Stars & Stars + Same Colored Symbol & Shapers & Rotated Shapers & Eraser +158481 - 0x288AA (Fourth Wooden Beam) - True - Stars & Shapers & Eraser +158482 - 0x17FB9 (Left Upstairs Single) - True - Stars & Dots & Full Dots +158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots & Full Dots & Black/White Squares +158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Dots & Full Dots & Stars +158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Full Dots & Shapers +158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Dots & Full Dots & Triangles +158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Dots & Full Dots & Triangles & Eraser +158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots +158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Black/White Squares & Colored Squares +158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol +158491 - 0x0008C (Right Upstairs Left Row 4) - 0x0008B - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Shapers +158492 - 0x0008A (Right Upstairs Left Row 5) - 0x0008C - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol +158493 - 0x00089 (Right Upstairs Left Row 6) - 0x0008A - Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Rotated Shapers +158494 - 0x0006A (Right Upstairs Left Row 7) - 0x00089 - Stars & Stars + Same Colored Symbol & Shapers & Negative Shapers +158495 - 0x0006C (Right Upstairs Left Row 8) - 0x0006A - Dots & Shapers & Negative Shapers & Eraser +158496 - 0x00027 (Right Upstairs Right Row 1) - True - Black/White Squares & Colored Squares & Eraser & Symmetry +158497 - 0x00028 (Right Upstairs Right Row 2) - 0x00027 - Black/White Squares & Colored Squares & Eraser & Symmetry +158498 - 0x00029 (Right Upstairs Right Row 3) - 0x00028 - Stars & Stars + Same Colored Symbol & Eraser & Symmetry +158476 - 0x09DD5 (Lone Pillar) - True - Triangles & Dots +Door - 0x019A5 (Pillar Door) - 0x09DD5 +158449 - 0x021D7 (Mountain Shortcut Panel) - True - Triangles +Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7 +158450 - 0x17CF2 (Swamp Shortcut Panel) - True - Triangles +Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2 +159341 - 0x3397C (Skylight EP) - True - True + +Caves Path to Challenge (Caves) - Challenge - 0x0A19A: +158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol & Triangles +Door - 0x0A19A (Challenge Entry) - 0x0A16E + +==Challenge== + +Challenge (Challenge) - Tunnels - 0x0348A - Challenge Vault - 0x04D75: +158499 - 0x0A332 (Start Timer) - 11 Lasers - True +158500 - 0x0088E (Small Basic) - 0x0A332 - True +158501 - 0x00BAF (Big Basic) - 0x0088E - True +158502 - 0x00BF3 (Square) - 0x00BAF - Black/White Squares +158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots +158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots +158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots +158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers +158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True +158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Black/White Squares +158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Black/White Squares +158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Black/White Squares +158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares +158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares +158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares +158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles +158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles +158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry +158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry +Door - 0x04D75 (Vault Door) - 0x1C31A & 0x1C319 +158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles & Dots +Door - 0x0348A (Tunnels Entry) - 0x039B4 +159530 - 0x28B30 (Water EP) - True - True + +Challenge Vault (Challenge): +158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True + +==Tunnels== + +Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Behind Elevator - 0x27263 - Town - 0x09E87: +158668 - 0x2FAF6 (Vault Box) - True - True +158519 - 0x27732 (Theater Shortcut Panel) - True - True +Door - 0x27739 (Theater Shortcut) - 0x27732 +158520 - 0x2773D (Desert Shortcut Panel) - True - True +Door - 0x27263 (Desert Shortcut) - 0x2773D +158521 - 0x09E85 (Town Shortcut Panel) - True - Triangles & Dots +Door - 0x09E87 (Town Shortcut) - 0x09E85 +159557 - 0x33A20 (Theater Flowers EP) - 0x03553 & Theater to Tunnels - True + +==Boat== + +The Ocean (Boat) - Main Island - TrueOneWay - Swamp Near Boat - TrueOneWay - Treehouse Entry Area - TrueOneWay - Quarry Boathouse Behind Staircase - TrueOneWay - Inside Glass Factory Behind Back Wall - TrueOneWay: +159042 - 0x22106 (Desert EP) - True - True +159223 - 0x03B25 (Shipwreck CCW Underside EP) - True - True +159231 - 0x28B29 (Shipwreck Green EP) - True - True +159232 - 0x28B2A (Shipwreck CW Underside EP) - True - True +159323 - 0x03D0D (Bunker Yellow Line EP) - True - True +159515 - 0x28A37 (Town Long Sewer EP) - True - True +159520 - 0x33857 (Tutorial EP) - True - True +159521 - 0x33879 (Tutorial Reflection EP) - True - True +159522 - 0x03C19 (Tutorial Moss EP) - True - True +159531 - 0x035C9 (Cargo Box EP) - 0x0A0C9 - True diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index b0d8fc3c4f6e..e5103ef3807e 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -13,6 +13,22 @@ # 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(): diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index 87e1015257c7..58f2e894e849 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -17,6 +17,7 @@ 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}'") diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 26df743592cd..737daff70fae 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -220,6 +220,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") diff --git a/worlds/witness/entity_hunt.py b/worlds/witness/entity_hunt.py index 34cf7d3d7f88..86881930c3e1 100644 --- a/worlds/witness/entity_hunt.py +++ b/worlds/witness/entity_hunt.py @@ -145,7 +145,7 @@ def _get_next_random_batch(self, amount: int, same_area_discouragement: float) - 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]) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 09c3f0b10192..99e8eea2eb89 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -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") @@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: - location_name += " (" + world.player_name + ")" + location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item @@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes item_name = item.name if item.player != world.player: - item_name += " (" + world.player_name + ")" + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" hint_text = "" area: Optional[str] = None diff --git a/worlds/witness/options.py b/worlds/witness/options.py index f91e5218c35e..4de966abe96d 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -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 diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 220574ff048e..4c98cb78495e 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -7,7 +7,6 @@ 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 @@ def get_early_items(self) -> List[str]: """ 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 diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 027d1834d99e..f8b7db3570a9 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -87,12 +87,14 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in 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 diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 105514c91eda..8993048065f4 100644 --- a/worlds/witness/presets.py +++ b/worlds/witness/presets.py @@ -3,6 +3,13 @@ from .options import * witness_option_presets: Dict[str, Dict[str, Any]] = { + # Best for beginners. This is just default options, but with a much easier goal that skips the Mountain puzzles. + "Beginner Mode": { + "victory_condition": VictoryCondition.option_mountain_box_short, + + "puzzle_skip_amount": 15, + }, + # Great for short syncs & scratching that "speedrun with light routing elements" itch. "Short & Dense": { "progression_balancing": 30, diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 7ff8c440ad86..1df438f68b0d 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -30,6 +30,8 @@ def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorl 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 diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 2f3210a21467..74ea2aef5740 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -214,7 +214,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version. """ - direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)] + direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)] if not direct_items: return requirement_option @@ -224,7 +224,7 @@ def optimize_requirement_option(requirement_option: List[Union[CollectionRule, S return [ rule for rule in requirement_option - if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]]) + if not (isinstance(rule, SimpleItemRepresentation) and rule[1] < max_per_item[rule[0]]) ] @@ -234,12 +234,6 @@ def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleIte Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules. If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts() """ - converted_sublist = [] - - for rule in requirement: - if not isinstance(rule, tuple): - converted_sublist.append(rule) - continue collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)] item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)] diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py index f09897ce4053..5e60dfc52172 100644 --- a/worlds/witness/test/test_lasers.py +++ b/worlds/witness/test/test_lasers.py @@ -96,6 +96,39 @@ def test_symbols_to_win(self) -> None: 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, diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py index bea278a04287..7473716e06e6 100644 --- a/worlds/witness/test/test_roll_other_options.py +++ b/worlds/witness/test/test_roll_other_options.py @@ -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", diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py index 8012480075a7..3be874f3c0eb 100644 --- a/worlds/witness/test/test_symbol_shuffle.py +++ b/worlds/witness/test/test_symbol_shuffle.py @@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase): { "puzzle_randomization": "none", }, + { + "puzzle_randomization": "umbra_variety", + } ] common_options = { @@ -63,12 +66,15 @@ def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None: 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) diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py index fa52c93ad6f2..c76dc538146e 100644 --- a/worlds/yachtdice/Items.py +++ b/worlds/yachtdice/Items.py @@ -16,8 +16,6 @@ class YachtDiceItem(Item): item_table = { - # victory item, always placed manually at goal location - "Victory": ItemData(16871244000 - 1, ItemClassification.progression), "Dice": ItemData(16871244000, ItemClassification.progression), "Dice Fragment": ItemData(16871244001, ItemClassification.progression), "Roll": ItemData(16871244002, ItemClassification.progression), diff --git a/worlds/yachtdice/Rules.py b/worlds/yachtdice/Rules.py index 1db5cebccdef..d99f5b147493 100644 --- a/worlds/yachtdice/Rules.py +++ b/worlds/yachtdice/Rules.py @@ -29,7 +29,7 @@ def mean_score(self, num_dice, num_rolls): 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: diff --git a/worlds/yachtdice/YachtWeights.py b/worlds/yachtdice/YachtWeights.py index ee387fdf212d..5f647f3420ba 100644 --- a/worlds/yachtdice/YachtWeights.py +++ b/worlds/yachtdice/YachtWeights.py @@ -17,77 +17,77 @@ ("Category Ones", 0, 7): {0: 100000}, ("Category Ones", 0, 8): {0: 100000}, ("Category Ones", 1, 0): {0: 100000}, - ("Category Ones", 1, 1): {0: 83416, 1: 16584}, - ("Category Ones", 1, 2): {0: 69346, 1: 30654}, - ("Category Ones", 1, 3): {0: 57756, 1: 42244}, - ("Category Ones", 1, 4): {0: 48709, 1: 51291}, - ("Category Ones", 1, 5): {0: 40214, 1: 59786}, + ("Category Ones", 1, 1): {0: 100000}, + ("Category Ones", 1, 2): {0: 100000}, + ("Category Ones", 1, 3): {0: 100000}, + ("Category Ones", 1, 4): {0: 100000}, + ("Category Ones", 1, 5): {0: 100000}, ("Category Ones", 1, 6): {0: 33491, 1: 66509}, ("Category Ones", 1, 7): {0: 27838, 1: 72162}, ("Category Ones", 1, 8): {0: 23094, 1: 76906}, ("Category Ones", 2, 0): {0: 100000}, - ("Category Ones", 2, 1): {0: 69715, 1: 30285}, - ("Category Ones", 2, 2): {0: 48066, 1: 51934}, - ("Category Ones", 2, 3): {0: 33544, 1: 48585, 2: 17871}, - ("Category Ones", 2, 4): {0: 23342, 1: 50092, 2: 26566}, - ("Category Ones", 2, 5): {0: 16036, 1: 48250, 2: 35714}, - ("Category Ones", 2, 6): {0: 11355, 1: 44545, 2: 44100}, - ("Category Ones", 2, 7): {0: 7812, 1: 40248, 2: 51940}, - ("Category Ones", 2, 8): {0: 5395, 1: 35484, 2: 59121}, + ("Category Ones", 2, 1): {0: 100000}, + ("Category Ones", 2, 2): {0: 100000}, + ("Category Ones", 2, 3): {0: 33544, 1: 66456}, + ("Category Ones", 2, 4): {0: 23342, 1: 76658}, + ("Category Ones", 2, 5): {0: 16036, 2: 83964}, + ("Category Ones", 2, 6): {0: 11355, 2: 88645}, + ("Category Ones", 2, 7): {0: 7812, 2: 92188}, + ("Category Ones", 2, 8): {0: 5395, 2: 94605}, ("Category Ones", 3, 0): {0: 100000}, - ("Category Ones", 3, 1): {0: 57462, 1: 42538}, - ("Category Ones", 3, 2): {0: 33327, 1: 44253, 2: 22420}, - ("Category Ones", 3, 3): {0: 19432, 1: 42237, 2: 38331}, - ("Category Ones", 3, 4): {0: 11191, 1: 36208, 2: 38606, 3: 13995}, - ("Category Ones", 3, 5): {0: 6536, 1: 28891, 2: 43130, 3: 21443}, - ("Category Ones", 3, 6): {0: 3697, 1: 22501, 2: 44196, 3: 29606}, - ("Category Ones", 3, 7): {0: 2134, 2: 60499, 3: 37367}, - ("Category Ones", 3, 8): {0: 1280, 2: 53518, 3: 45202}, + ("Category Ones", 3, 1): {0: 100000}, + ("Category Ones", 3, 2): {0: 33327, 1: 66673}, + ("Category Ones", 3, 3): {0: 19432, 2: 80568}, + ("Category Ones", 3, 4): {0: 11191, 2: 88809}, + ("Category Ones", 3, 5): {0: 35427, 2: 64573}, + ("Category Ones", 3, 6): {0: 26198, 2: 73802}, + ("Category Ones", 3, 7): {0: 18851, 3: 81149}, + ("Category Ones", 3, 8): {0: 13847, 3: 86153}, ("Category Ones", 4, 0): {0: 100000}, - ("Category Ones", 4, 1): {0: 48178, 1: 38635, 2: 13187}, - ("Category Ones", 4, 2): {0: 23349, 1: 40775, 2: 35876}, - ("Category Ones", 4, 3): {0: 11366, 1: 32547, 2: 35556, 3: 20531}, - ("Category Ones", 4, 4): {0: 5331, 1: 23241, 2: 37271, 3: 34157}, - ("Category Ones", 4, 5): {0: 2640, 2: 49872, 3: 47488}, - ("Category Ones", 4, 6): {0: 1253, 2: 39816, 3: 39298, 4: 19633}, - ("Category Ones", 4, 7): {0: 6915, 2: 24313, 3: 41680, 4: 27092}, - ("Category Ones", 4, 8): {0: 4228, 3: 61312, 4: 34460}, + ("Category Ones", 4, 1): {0: 100000}, + ("Category Ones", 4, 2): {0: 23349, 2: 76651}, + ("Category Ones", 4, 3): {0: 11366, 2: 88634}, + ("Category Ones", 4, 4): {0: 28572, 3: 71428}, + ("Category Ones", 4, 5): {0: 17976, 3: 82024}, + ("Category Ones", 4, 6): {0: 1253, 3: 98747}, + ("Category Ones", 4, 7): {0: 31228, 3: 68772}, + ("Category Ones", 4, 8): {0: 23273, 4: 76727}, ("Category Ones", 5, 0): {0: 100000}, - ("Category Ones", 5, 1): {0: 40042, 1: 40202, 2: 19756}, - ("Category Ones", 5, 2): {0: 16212, 1: 35432, 2: 31231, 3: 17125}, - ("Category Ones", 5, 3): {0: 6556, 1: 23548, 2: 34509, 3: 35387}, - ("Category Ones", 5, 4): {0: 2552, 2: 44333, 3: 32048, 4: 21067}, - ("Category Ones", 5, 5): {0: 8783, 2: 23245, 3: 34614, 4: 33358}, - ("Category Ones", 5, 6): {0: 4513, 3: 49603, 4: 32816, 5: 13068}, - ("Category Ones", 5, 7): {0: 2295, 3: 40470, 4: 37869, 5: 19366}, - ("Category Ones", 5, 8): {0: 73, 3: 33115, 4: 40166, 5: 26646}, + ("Category Ones", 5, 1): {0: 100000}, + ("Category Ones", 5, 2): {0: 16212, 2: 83788}, + ("Category Ones", 5, 3): {0: 30104, 3: 69896}, + ("Category Ones", 5, 4): {0: 2552, 3: 97448}, + ("Category Ones", 5, 5): {0: 32028, 4: 67972}, + ("Category Ones", 5, 6): {0: 21215, 4: 78785}, + ("Category Ones", 5, 7): {0: 2295, 4: 97705}, + ("Category Ones", 5, 8): {0: 1167, 4: 98833}, ("Category Ones", 6, 0): {0: 100000}, - ("Category Ones", 6, 1): {0: 33501, 1: 40042, 2: 26457}, - ("Category Ones", 6, 2): {0: 11326, 1: 29379, 2: 32368, 3: 26927}, - ("Category Ones", 6, 3): {0: 3764, 2: 46660, 3: 28928, 4: 20648}, - ("Category Ones", 6, 4): {0: 1231, 2: 29883, 3: 31038, 4: 37848}, - ("Category Ones", 6, 5): {0: 4208, 3: 41897, 4: 30878, 5: 23017}, - ("Category Ones", 6, 6): {0: 1850, 3: 30396, 4: 33022, 5: 34732}, - ("Category Ones", 6, 7): {0: 5503, 4: 48099, 5: 32432, 6: 13966}, - ("Category Ones", 6, 8): {0: 2896, 4: 39616, 5: 37005, 6: 20483}, + ("Category Ones", 6, 1): {0: 33501, 1: 66499}, + ("Category Ones", 6, 2): {0: 40705, 2: 59295}, + ("Category Ones", 6, 3): {0: 3764, 3: 96236}, + ("Category Ones", 6, 4): {0: 9324, 4: 90676}, + ("Category Ones", 6, 5): {0: 4208, 4: 95792}, + ("Category Ones", 6, 6): {0: 158, 5: 99842}, + ("Category Ones", 6, 7): {0: 5503, 5: 94497}, + ("Category Ones", 6, 8): {0: 2896, 5: 97104}, ("Category Ones", 7, 0): {0: 100000}, - ("Category Ones", 7, 1): {0: 27838, 1: 39224, 2: 32938}, - ("Category Ones", 7, 2): {0: 7796, 1: 23850, 2: 31678, 3: 23224, 4: 13452}, - ("Category Ones", 7, 3): {0: 2247, 2: 35459, 3: 29131, 4: 33163}, - ("Category Ones", 7, 4): {0: 5252, 3: 41207, 4: 28065, 5: 25476}, - ("Category Ones", 7, 5): {0: 174, 3: 29347, 4: 28867, 5: 26190, 6: 15422}, - ("Category Ones", 7, 6): {0: 4625, 4: 38568, 5: 30596, 6: 26211}, - ("Category Ones", 7, 7): {0: 230, 4: 30109, 5: 32077, 6: 37584}, - ("Category Ones", 7, 8): {0: 5519, 5: 45718, 6: 33357, 7: 15406}, + ("Category Ones", 7, 1): {0: 27838, 2: 72162}, + ("Category Ones", 7, 2): {0: 7796, 3: 92204}, + ("Category Ones", 7, 3): {0: 13389, 4: 86611}, + ("Category Ones", 7, 4): {0: 5252, 4: 94748}, + ("Category Ones", 7, 5): {0: 9854, 5: 90146}, + ("Category Ones", 7, 6): {0: 4625, 5: 95375}, + ("Category Ones", 7, 7): {0: 30339, 6: 69661}, + ("Category Ones", 7, 8): {0: 5519, 6: 94481}, ("Category Ones", 8, 0): {0: 100000}, - ("Category Ones", 8, 1): {0: 23156, 1: 37295, 2: 26136, 3: 13413}, - ("Category Ones", 8, 2): {0: 5472, 2: 48372, 3: 25847, 4: 20309}, - ("Category Ones", 8, 3): {0: 8661, 3: 45896, 4: 24664, 5: 20779}, - ("Category Ones", 8, 4): {0: 2807, 3: 29707, 4: 27157, 5: 23430, 6: 16899}, - ("Category Ones", 8, 5): {0: 5173, 4: 36033, 5: 27792, 6: 31002}, - ("Category Ones", 8, 6): {0: 255, 4: 25642, 5: 27508, 6: 27112, 7: 19483}, - ("Category Ones", 8, 7): {0: 4236, 5: 35323, 6: 30438, 7: 30003}, - ("Category Ones", 8, 8): {0: 310, 5: 27692, 6: 30830, 7: 41168}, + ("Category Ones", 8, 1): {0: 23156, 2: 76844}, + ("Category Ones", 8, 2): {0: 5472, 3: 94528}, + ("Category Ones", 8, 3): {0: 8661, 4: 91339}, + ("Category Ones", 8, 4): {0: 12125, 5: 87875}, + ("Category Ones", 8, 5): {0: 5173, 5: 94827}, + ("Category Ones", 8, 6): {0: 8872, 6: 91128}, + ("Category Ones", 8, 7): {0: 4236, 6: 95764}, + ("Category Ones", 8, 8): {0: 9107, 7: 90893}, ("Category Twos", 0, 0): {0: 100000}, ("Category Twos", 0, 1): {0: 100000}, ("Category Twos", 0, 2): {0: 100000}, @@ -98,8 +98,8 @@ ("Category Twos", 0, 7): {0: 100000}, ("Category Twos", 0, 8): {0: 100000}, ("Category Twos", 1, 0): {0: 100000}, - ("Category Twos", 1, 1): {0: 83475, 2: 16525}, - ("Category Twos", 1, 2): {0: 69690, 2: 30310}, + ("Category Twos", 1, 1): {0: 100000}, + ("Category Twos", 1, 2): {0: 100000}, ("Category Twos", 1, 3): {0: 57818, 2: 42182}, ("Category Twos", 1, 4): {0: 48418, 2: 51582}, ("Category Twos", 1, 5): {0: 40301, 2: 59699}, @@ -107,68 +107,68 @@ ("Category Twos", 1, 7): {0: 28182, 2: 71818}, ("Category Twos", 1, 8): {0: 23406, 2: 76594}, ("Category Twos", 2, 0): {0: 100000}, - ("Category Twos", 2, 1): {0: 69724, 2: 30276}, - ("Category Twos", 2, 2): {0: 48238, 2: 42479, 4: 9283}, - ("Category Twos", 2, 3): {0: 33290, 2: 48819, 4: 17891}, - ("Category Twos", 2, 4): {0: 23136, 2: 49957, 4: 26907}, - ("Category Twos", 2, 5): {0: 16146, 2: 48200, 4: 35654}, - ("Category Twos", 2, 6): {0: 11083, 2: 44497, 4: 44420}, - ("Category Twos", 2, 7): {0: 7662, 2: 40343, 4: 51995}, - ("Category Twos", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Twos", 2, 1): {0: 100000}, + ("Category Twos", 2, 2): {0: 48238, 2: 51762}, + ("Category Twos", 2, 3): {0: 33290, 4: 66710}, + ("Category Twos", 2, 4): {0: 23136, 4: 76864}, + ("Category Twos", 2, 5): {0: 16146, 4: 83854}, + ("Category Twos", 2, 6): {0: 11083, 4: 88917}, + ("Category Twos", 2, 7): {0: 7662, 4: 92338}, + ("Category Twos", 2, 8): {0: 5354, 4: 94646}, ("Category Twos", 3, 0): {0: 100000}, - ("Category Twos", 3, 1): {0: 58021, 2: 34522, 4: 7457}, - ("Category Twos", 3, 2): {0: 33548, 2: 44261, 4: 22191}, - ("Category Twos", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, - ("Category Twos", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, - ("Category Twos", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, - ("Category Twos", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, - ("Category Twos", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, - ("Category Twos", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Twos", 3, 1): {0: 58021, 2: 41979}, + ("Category Twos", 3, 2): {0: 33548, 4: 66452}, + ("Category Twos", 3, 3): {0: 19375, 4: 80625}, + ("Category Twos", 3, 4): {0: 10998, 4: 89002}, + ("Category Twos", 3, 5): {0: 6519, 6: 93481}, + ("Category Twos", 3, 6): {0: 3619, 6: 96381}, + ("Category Twos", 3, 7): {0: 2195, 6: 97805}, + ("Category Twos", 3, 8): {0: 13675, 6: 86325}, ("Category Twos", 4, 0): {0: 100000}, - ("Category Twos", 4, 1): {0: 48235, 2: 38602, 4: 13163}, - ("Category Twos", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, - ("Category Twos", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, - ("Category Twos", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, - ("Category Twos", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, - ("Category Twos", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, - ("Category Twos", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, - ("Category Twos", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Twos", 4, 1): {0: 48235, 2: 51765}, + ("Category Twos", 4, 2): {0: 23289, 4: 76711}, + ("Category Twos", 4, 3): {0: 11177, 6: 88823}, + ("Category Twos", 4, 4): {0: 5499, 6: 94501}, + ("Category Twos", 4, 5): {0: 18356, 6: 81644}, + ("Category Twos", 4, 6): {0: 11169, 8: 88831}, + ("Category Twos", 4, 7): {0: 6945, 8: 93055}, + ("Category Twos", 4, 8): {0: 4091, 8: 95909}, ("Category Twos", 5, 0): {0: 100000}, - ("Category Twos", 5, 1): {0: 40028, 2: 40241, 4: 19731}, - ("Category Twos", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, - ("Category Twos", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, - ("Category Twos", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, - ("Category Twos", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, - ("Category Twos", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, - ("Category Twos", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, - ("Category Twos", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Twos", 5, 1): {0: 40028, 4: 59972}, + ("Category Twos", 5, 2): {0: 16009, 6: 83991}, + ("Category Twos", 5, 3): {0: 6489, 6: 93511}, + ("Category Twos", 5, 4): {0: 16690, 8: 83310}, + ("Category Twos", 5, 5): {0: 9016, 8: 90984}, + ("Category Twos", 5, 6): {0: 4602, 8: 95398}, + ("Category Twos", 5, 7): {0: 13627, 10: 86373}, + ("Category Twos", 5, 8): {0: 8742, 10: 91258}, ("Category Twos", 6, 0): {0: 100000}, - ("Category Twos", 6, 1): {0: 33502, 2: 40413, 4: 26085}, - ("Category Twos", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, - ("Category Twos", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, - ("Category Twos", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, - ("Category Twos", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, - ("Category Twos", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, - ("Category Twos", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, - ("Category Twos", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Twos", 6, 1): {0: 33502, 4: 66498}, + ("Category Twos", 6, 2): {0: 11210, 6: 88790}, + ("Category Twos", 6, 3): {0: 3673, 6: 96327}, + ("Category Twos", 6, 4): {0: 9291, 8: 90709}, + ("Category Twos", 6, 5): {0: 441, 8: 99559}, + ("Category Twos", 6, 6): {0: 10255, 10: 89745}, + ("Category Twos", 6, 7): {0: 5646, 10: 94354}, + ("Category Twos", 6, 8): {0: 14287, 12: 85713}, ("Category Twos", 7, 0): {0: 100000}, - ("Category Twos", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, - ("Category Twos", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, - ("Category Twos", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, - ("Category Twos", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, - ("Category Twos", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, - ("Category Twos", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, - ("Category Twos", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, - ("Category Twos", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Twos", 7, 1): {0: 27683, 4: 72317}, + ("Category Twos", 7, 2): {0: 7824, 6: 92176}, + ("Category Twos", 7, 3): {0: 13167, 8: 86833}, + ("Category Twos", 7, 4): {0: 564, 10: 99436}, + ("Category Twos", 7, 5): {0: 9824, 10: 90176}, + ("Category Twos", 7, 6): {0: 702, 12: 99298}, + ("Category Twos", 7, 7): {0: 10186, 12: 89814}, + ("Category Twos", 7, 8): {0: 942, 12: 99058}, ("Category Twos", 8, 0): {0: 100000}, - ("Category Twos", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, - ("Category Twos", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, - ("Category Twos", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, - ("Category Twos", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, - ("Category Twos", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, - ("Category Twos", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, - ("Category Twos", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, - ("Category Twos", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Twos", 8, 1): {0: 23378, 4: 76622}, + ("Category Twos", 8, 2): {0: 5420, 8: 94580}, + ("Category Twos", 8, 3): {0: 8560, 10: 91440}, + ("Category Twos", 8, 4): {0: 12199, 12: 87801}, + ("Category Twos", 8, 5): {0: 879, 12: 99121}, + ("Category Twos", 8, 6): {0: 9033, 14: 90967}, + ("Category Twos", 8, 7): {0: 15767, 14: 84233}, + ("Category Twos", 8, 8): {2: 9033, 14: 90967}, ("Category Threes", 0, 0): {0: 100000}, ("Category Threes", 0, 1): {0: 100000}, ("Category Threes", 0, 2): {0: 100000}, @@ -179,7 +179,7 @@ ("Category Threes", 0, 7): {0: 100000}, ("Category Threes", 0, 8): {0: 100000}, ("Category Threes", 1, 0): {0: 100000}, - ("Category Threes", 1, 1): {0: 83343, 3: 16657}, + ("Category Threes", 1, 1): {0: 100000}, ("Category Threes", 1, 2): {0: 69569, 3: 30431}, ("Category Threes", 1, 3): {0: 57872, 3: 42128}, ("Category Threes", 1, 4): {0: 48081, 3: 51919}, @@ -189,67 +189,67 @@ ("Category Threes", 1, 8): {0: 23240, 3: 76760}, ("Category Threes", 2, 0): {0: 100000}, ("Category Threes", 2, 1): {0: 69419, 3: 30581}, - ("Category Threes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, - ("Category Threes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, - ("Category Threes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, - ("Category Threes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, - ("Category Threes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, - ("Category Threes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, - ("Category Threes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Threes", 2, 2): {0: 48202, 3: 51798}, + ("Category Threes", 2, 3): {0: 33376, 6: 66624}, + ("Category Threes", 2, 4): {0: 23276, 6: 76724}, + ("Category Threes", 2, 5): {0: 16092, 6: 83908}, + ("Category Threes", 2, 6): {0: 11232, 6: 88768}, + ("Category Threes", 2, 7): {0: 7589, 6: 92411}, + ("Category Threes", 2, 8): {0: 5447, 6: 94553}, ("Category Threes", 3, 0): {0: 100000}, - ("Category Threes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, - ("Category Threes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, - ("Category Threes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, - ("Category Threes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, - ("Category Threes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, - ("Category Threes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, - ("Category Threes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, - ("Category Threes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Threes", 3, 1): {0: 57964, 3: 42036}, + ("Category Threes", 3, 2): {0: 33637, 6: 66363}, + ("Category Threes", 3, 3): {0: 19520, 6: 80480}, + ("Category Threes", 3, 4): {0: 11265, 6: 88735}, + ("Category Threes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, + ("Category Threes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, + ("Category Threes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, + ("Category Threes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, ("Category Threes", 4, 0): {0: 100000}, - ("Category Threes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, - ("Category Threes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, - ("Category Threes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, - ("Category Threes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, - ("Category Threes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, - ("Category Threes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, - ("Category Threes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, - ("Category Threes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Threes", 4, 1): {0: 48121, 6: 51879}, + ("Category Threes", 4, 2): {0: 23296, 6: 76704}, + ("Category Threes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, + ("Category Threes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, + ("Category Threes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, + ("Category Threes", 4, 6): {0: 11267, 9: 88733}, + ("Category Threes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, + ("Category Threes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, ("Category Threes", 5, 0): {0: 100000}, - ("Category Threes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, - ("Category Threes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, - ("Category Threes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, - ("Category Threes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, - ("Category Threes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, - ("Category Threes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, - ("Category Threes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, - ("Category Threes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Threes", 5, 1): {0: 40183, 6: 59817}, + ("Category Threes", 5, 2): {0: 16197, 6: 83803}, + ("Category Threes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, + ("Category Threes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, + ("Category Threes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, + ("Category Threes", 5, 6): {0: 4652, 12: 95348}, + ("Category Threes", 5, 7): {0: 2365, 12: 97635}, + ("Category Threes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, ("Category Threes", 6, 0): {0: 100000}, - ("Category Threes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, - ("Category Threes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, - ("Category Threes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, - ("Category Threes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, - ("Category Threes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, - ("Category Threes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, - ("Category Threes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, - ("Category Threes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Threes", 6, 1): {0: 33473, 6: 66527}, + ("Category Threes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, + ("Category Threes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, + ("Category Threes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, + ("Category Threes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, + ("Category Threes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, + ("Category Threes", 6, 7): {0: 5519, 15: 94481}, + ("Category Threes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, ("Category Threes", 7, 0): {0: 100000}, - ("Category Threes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, - ("Category Threes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, - ("Category Threes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, - ("Category Threes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, - ("Category Threes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, - ("Category Threes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, - ("Category Threes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, - ("Category Threes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Threes", 7, 1): {0: 27933, 6: 72067}, + ("Category Threes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, + ("Category Threes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, + ("Category Threes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, + ("Category Threes", 7, 5): {0: 9894, 15: 90106}, + ("Category Threes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, + ("Category Threes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, + ("Category Threes", 7, 8): {0: 5710, 18: 94290}, ("Category Threes", 8, 0): {0: 100000}, - ("Category Threes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, - ("Category Threes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, - ("Category Threes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, - ("Category Threes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, - ("Category Threes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, - ("Category Threes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, - ("Category Threes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, - ("Category Threes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, + ("Category Threes", 8, 1): {0: 23337, 6: 76663}, + ("Category Threes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, + ("Category Threes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, + ("Category Threes", 8, 4): {0: 291, 12: 59487, 18: 40222}, + ("Category Threes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, + ("Category Threes", 8, 6): {0: 8804, 18: 91196}, + ("Category Threes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, + ("Category Threes", 8, 8): {0: 9252, 21: 90748}, ("Category Fours", 0, 0): {0: 100000}, ("Category Fours", 0, 1): {0: 100000}, ("Category Fours", 0, 2): {0: 100000}, @@ -270,67 +270,67 @@ ("Category Fours", 1, 8): {0: 23431, 4: 76569}, ("Category Fours", 2, 0): {0: 100000}, ("Category Fours", 2, 1): {0: 69379, 4: 30621}, - ("Category Fours", 2, 2): {0: 48538, 4: 42240, 8: 9222}, + ("Category Fours", 2, 2): {0: 48538, 4: 51462}, ("Category Fours", 2, 3): {0: 33756, 4: 48555, 8: 17689}, ("Category Fours", 2, 4): {0: 23070, 4: 49916, 8: 27014}, ("Category Fours", 2, 5): {0: 16222, 4: 48009, 8: 35769}, ("Category Fours", 2, 6): {0: 11125, 4: 44400, 8: 44475}, ("Category Fours", 2, 7): {0: 7919, 4: 40216, 8: 51865}, - ("Category Fours", 2, 8): {0: 5348, 4: 35757, 8: 58895}, + ("Category Fours", 2, 8): {0: 5348, 8: 94652}, ("Category Fours", 3, 0): {0: 100000}, - ("Category Fours", 3, 1): {0: 57914, 4: 34622, 8: 7464}, + ("Category Fours", 3, 1): {0: 57914, 4: 42086}, ("Category Fours", 3, 2): {0: 33621, 4: 44110, 8: 22269}, - ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 30898, 12: 7524}, - ("Category Fours", 3, 4): {0: 11125, 4: 36011, 8: 39024, 12: 13840}, - ("Category Fours", 3, 5): {0: 6367, 4: 29116, 8: 43192, 12: 21325}, - ("Category Fours", 3, 6): {0: 3643, 4: 22457, 8: 44477, 12: 29423}, - ("Category Fours", 3, 7): {0: 2178, 4: 16802, 8: 43275, 12: 37745}, - ("Category Fours", 3, 8): {0: 1255, 4: 12301, 8: 41132, 12: 45312}, + ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 38422}, + ("Category Fours", 3, 4): {0: 11125, 8: 88875}, + ("Category Fours", 3, 5): {0: 6367, 8: 72308, 12: 21325}, + ("Category Fours", 3, 6): {0: 3643, 8: 66934, 12: 29423}, + ("Category Fours", 3, 7): {0: 2178, 8: 60077, 12: 37745}, + ("Category Fours", 3, 8): {0: 1255, 8: 53433, 12: 45312}, ("Category Fours", 4, 0): {0: 100000}, - ("Category Fours", 4, 1): {0: 48465, 4: 38398, 8: 13137}, - ("Category Fours", 4, 2): {0: 23296, 4: 40911, 8: 27073, 12: 8720}, - ("Category Fours", 4, 3): {0: 11200, 4: 33191, 8: 35337, 12: 20272}, - ("Category Fours", 4, 4): {0: 5447, 4: 23066, 8: 37441, 12: 26861, 16: 7185}, - ("Category Fours", 4, 5): {0: 2533, 4: 15668, 8: 34781, 12: 34222, 16: 12796}, - ("Category Fours", 4, 6): {0: 1314, 4: 10001, 8: 29850, 12: 39425, 16: 19410}, - ("Category Fours", 4, 7): {0: 592, 4: 6231, 8: 24250, 12: 41917, 16: 27010}, - ("Category Fours", 4, 8): {0: 302, 8: 23055, 12: 41866, 16: 34777}, + ("Category Fours", 4, 1): {0: 48465, 4: 51535}, + ("Category Fours", 4, 2): {0: 23296, 4: 40911, 12: 35793}, + ("Category Fours", 4, 3): {0: 11200, 8: 68528, 12: 20272}, + ("Category Fours", 4, 4): {0: 5447, 8: 60507, 12: 34046}, + ("Category Fours", 4, 5): {0: 2533, 8: 50449, 16: 47018}, + ("Category Fours", 4, 6): {0: 1314, 8: 39851, 12: 39425, 16: 19410}, + ("Category Fours", 4, 7): {0: 6823, 12: 66167, 16: 27010}, + ("Category Fours", 4, 8): {0: 4189, 12: 61034, 16: 34777}, ("Category Fours", 5, 0): {0: 100000}, - ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 16028, 12: 3630}, - ("Category Fours", 5, 2): {0: 15946, 4: 35579, 8: 31158, 12: 13998, 16: 3319}, - ("Category Fours", 5, 3): {0: 6479, 4: 23705, 8: 34575, 12: 24783, 16: 10458}, - ("Category Fours", 5, 4): {0: 2635, 4: 13889, 8: 30079, 12: 32428, 16: 17263, 20: 3706}, - ("Category Fours", 5, 5): {0: 1160, 4: 7756, 8: 23332, 12: 34254, 16: 25803, 20: 7695}, - ("Category Fours", 5, 6): {0: 434, 8: 20773, 12: 32910, 16: 32752, 20: 13131}, - ("Category Fours", 5, 7): {0: 169, 8: 13536, 12: 29123, 16: 37701, 20: 19471}, - ("Category Fours", 5, 8): {0: 1267, 8: 7340, 12: 24807, 16: 40144, 20: 26442}, + ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 19658}, + ("Category Fours", 5, 2): {0: 15946, 8: 66737, 12: 17317}, + ("Category Fours", 5, 3): {0: 6479, 8: 58280, 16: 35241}, + ("Category Fours", 5, 4): {0: 2635, 8: 43968, 16: 53397}, + ("Category Fours", 5, 5): {0: 8916, 12: 57586, 16: 33498}, + ("Category Fours", 5, 6): {0: 4682, 12: 49435, 20: 45883}, + ("Category Fours", 5, 7): {0: 2291, 12: 40537, 16: 37701, 20: 19471}, + ("Category Fours", 5, 8): {0: 75, 16: 73483, 20: 26442}, ("Category Fours", 6, 0): {0: 100000}, - ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 20225, 12: 6287}, - ("Category Fours", 6, 2): {0: 11175, 4: 29824, 8: 32381, 12: 19179, 16: 7441}, - ("Category Fours", 6, 3): {0: 3698, 4: 16329, 8: 29939, 12: 29071, 16: 15808, 20: 5155}, - ("Category Fours", 6, 4): {0: 1284, 4: 7889, 8: 21748, 12: 31107, 16: 25281, 20: 12691}, - ("Category Fours", 6, 5): {0: 462, 8: 17601, 12: 27817, 16: 31233, 20: 18386, 24: 4501}, - ("Category Fours", 6, 6): {0: 1783, 8: 8344, 12: 22156, 16: 32690, 20: 26192, 24: 8835}, - ("Category Fours", 6, 7): {0: 767, 12: 20974, 16: 31490, 20: 32639, 24: 14130}, - ("Category Fours", 6, 8): {0: 357, 12: 13912, 16: 27841, 20: 37380, 24: 20510}, + ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 26512}, + ("Category Fours", 6, 2): {0: 11175, 8: 62205, 12: 26620}, + ("Category Fours", 6, 3): {0: 3698, 8: 46268, 16: 50034}, + ("Category Fours", 6, 4): {0: 9173, 12: 52855, 20: 37972}, + ("Category Fours", 6, 5): {0: 4254, 12: 41626, 20: 54120}, + ("Category Fours", 6, 6): {0: 1783, 16: 63190, 24: 35027}, + ("Category Fours", 6, 7): {0: 5456, 16: 47775, 24: 46769}, + ("Category Fours", 6, 8): {0: 2881, 16: 39229, 24: 57890}, ("Category Fours", 7, 0): {0: 100000}, - ("Category Fours", 7, 1): {0: 27821, 4: 39289, 8: 23327, 12: 9563}, - ("Category Fours", 7, 2): {0: 7950, 4: 24026, 8: 31633, 12: 23169, 16: 13222}, - ("Category Fours", 7, 3): {0: 2194, 4: 11153, 8: 24107, 12: 29411, 16: 21390, 20: 11745}, - ("Category Fours", 7, 4): {0: 560, 8: 19291, 12: 26330, 16: 28118, 20: 18174, 24: 7527}, - ("Category Fours", 7, 5): {0: 1858, 8: 7862, 12: 19425, 16: 29003, 20: 26113, 24: 15739}, - ("Category Fours", 7, 6): {0: 679, 12: 16759, 16: 25831, 20: 30724, 24: 20147, 28: 5860}, - ("Category Fours", 7, 7): {0: 13, 12: 10063, 16: 20524, 20: 31843, 24: 27368, 28: 10189}, - ("Category Fours", 7, 8): {4: 864, 16: 19910, 20: 30153, 24: 33428, 28: 15645}, + ("Category Fours", 7, 1): {0: 27821, 4: 39289, 12: 32890}, + ("Category Fours", 7, 2): {0: 7950, 8: 55659, 16: 36391}, + ("Category Fours", 7, 3): {0: 2194, 12: 64671, 20: 33135}, + ("Category Fours", 7, 4): {0: 5063, 12: 41118, 20: 53819}, + ("Category Fours", 7, 5): {0: 171, 16: 57977, 24: 41852}, + ("Category Fours", 7, 6): {0: 4575, 16: 38694, 24: 56731}, + ("Category Fours", 7, 7): {0: 252, 20: 62191, 28: 37557}, + ("Category Fours", 7, 8): {4: 5576, 20: 45351, 28: 49073}, ("Category Fours", 8, 0): {0: 100000}, - ("Category Fours", 8, 1): {0: 23275, 4: 37161, 8: 25964, 12: 13600}, - ("Category Fours", 8, 2): {0: 5421, 4: 19014, 8: 29259, 12: 25812, 16: 14387, 20: 6107}, - ("Category Fours", 8, 3): {0: 1277, 4: 7349, 8: 18330, 12: 27186, 16: 25138, 20: 14371, 24: 6349}, - ("Category Fours", 8, 4): {0: 289, 8: 11929, 12: 20282, 16: 26960, 20: 23292, 24: 12927, 28: 4321}, - ("Category Fours", 8, 5): {0: 835, 12: 16706, 16: 23588, 20: 27754, 24: 20767, 28: 10350}, - ("Category Fours", 8, 6): {0: 21, 12: 8911, 16: 17296, 20: 27398, 24: 27074, 28: 15457, 32: 3843}, - ("Category Fours", 8, 7): {0: 745, 16: 15069, 20: 23737, 24: 30628, 28: 22590, 32: 7231}, - ("Category Fours", 8, 8): {0: 1949, 16: 7021, 20: 18630, 24: 31109, 28: 29548, 32: 11743}, + ("Category Fours", 8, 1): {0: 23275, 8: 76725}, + ("Category Fours", 8, 2): {0: 5421, 8: 48273, 16: 46306}, + ("Category Fours", 8, 3): {0: 8626, 12: 45516, 20: 45858}, + ("Category Fours", 8, 4): {0: 2852, 16: 56608, 24: 40540}, + ("Category Fours", 8, 5): {0: 5049, 20: 63834, 28: 31117}, + ("Category Fours", 8, 6): {0: 269, 20: 53357, 28: 46374}, + ("Category Fours", 8, 7): {0: 4394, 24: 65785, 28: 29821}, + ("Category Fours", 8, 8): {0: 266, 24: 58443, 32: 41291}, ("Category Fives", 0, 0): {0: 100000}, ("Category Fives", 0, 1): {0: 100000}, ("Category Fives", 0, 2): {0: 100000}, @@ -350,8 +350,8 @@ ("Category Fives", 1, 7): {0: 27730, 5: 72270}, ("Category Fives", 1, 8): {0: 23210, 5: 76790}, ("Category Fives", 2, 0): {0: 100000}, - ("Category Fives", 2, 1): {0: 69299, 5: 27864, 10: 2837}, - ("Category Fives", 2, 2): {0: 48156, 5: 42526, 10: 9318}, + ("Category Fives", 2, 1): {0: 69299, 5: 30701}, + ("Category Fives", 2, 2): {0: 48156, 5: 51844}, ("Category Fives", 2, 3): {0: 33225, 5: 49153, 10: 17622}, ("Category Fives", 2, 4): {0: 23218, 5: 50075, 10: 26707}, ("Category Fives", 2, 5): {0: 15939, 5: 48313, 10: 35748}, @@ -359,59 +359,59 @@ ("Category Fives", 2, 7): {0: 7822, 5: 40388, 10: 51790}, ("Category Fives", 2, 8): {0: 5386, 5: 35636, 10: 58978}, ("Category Fives", 3, 0): {0: 100000}, - ("Category Fives", 3, 1): {0: 58034, 5: 34541, 10: 7425}, - ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 19403, 15: 2904}, - ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 30794, 15: 7492}, + ("Category Fives", 3, 1): {0: 58034, 5: 41966}, + ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 22307}, + ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 38286}, ("Category Fives", 3, 4): {0: 11196, 5: 36192, 10: 38673, 15: 13939}, - ("Category Fives", 3, 5): {0: 6561, 5: 29163, 10: 43014, 15: 21262}, - ("Category Fives", 3, 6): {0: 3719, 5: 22181, 10: 44611, 15: 29489}, - ("Category Fives", 3, 7): {0: 2099, 5: 16817, 10: 43466, 15: 37618}, - ("Category Fives", 3, 8): {0: 1281, 5: 12473, 10: 40936, 15: 45310}, + ("Category Fives", 3, 5): {0: 6561, 10: 72177, 15: 21262}, + ("Category Fives", 3, 6): {0: 3719, 10: 66792, 15: 29489}, + ("Category Fives", 3, 7): {0: 2099, 10: 60283, 15: 37618}, + ("Category Fives", 3, 8): {0: 1281, 10: 53409, 15: 45310}, ("Category Fives", 4, 0): {0: 100000}, ("Category Fives", 4, 1): {0: 48377, 5: 38345, 10: 13278}, - ("Category Fives", 4, 2): {0: 23126, 5: 40940, 10: 27041, 15: 8893}, - ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 17250, 20: 3208}, - ("Category Fives", 4, 4): {0: 5362, 5: 23073, 10: 37379, 15: 26968, 20: 7218}, - ("Category Fives", 4, 5): {0: 2655, 5: 15662, 10: 34602, 15: 34186, 20: 12895}, - ("Category Fives", 4, 6): {0: 1291, 5: 9959, 10: 29833, 15: 39417, 20: 19500}, - ("Category Fives", 4, 7): {0: 623, 5: 6231, 10: 24360, 15: 41779, 20: 27007}, - ("Category Fives", 4, 8): {0: 313, 10: 23001, 15: 41957, 20: 34729}, + ("Category Fives", 4, 2): {0: 23126, 5: 40940, 15: 35934}, + ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 20458}, + ("Category Fives", 4, 4): {0: 5362, 10: 60452, 20: 34186}, + ("Category Fives", 4, 5): {0: 2655, 10: 50264, 15: 34186, 20: 12895}, + ("Category Fives", 4, 6): {0: 1291, 10: 39792, 15: 39417, 20: 19500}, + ("Category Fives", 4, 7): {0: 6854, 15: 66139, 20: 27007}, + ("Category Fives", 4, 8): {0: 4150, 15: 61121, 20: 34729}, ("Category Fives", 5, 0): {0: 100000}, - ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 16029, 15: 3499}, - ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 13793, 20: 3266}, - ("Category Fives", 5, 3): {0: 6526, 5: 23716, 10: 34430, 15: 25017, 20: 10311}, - ("Category Fives", 5, 4): {0: 2615, 5: 13975, 10: 30133, 15: 32247, 20: 17219, 25: 3811}, - ("Category Fives", 5, 5): {0: 1063, 5: 7876, 10: 23203, 15: 34489, 20: 25757, 25: 7612}, - ("Category Fives", 5, 6): {0: 429, 5: 4091, 10: 16696, 15: 32855, 20: 32891, 25: 13038}, - ("Category Fives", 5, 7): {0: 159, 10: 13509, 15: 29416, 20: 37778, 25: 19138}, - ("Category Fives", 5, 8): {0: 1179, 10: 7453, 15: 24456, 20: 40615, 25: 26297}, + ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 19528}, + ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 17059}, + ("Category Fives", 5, 3): {0: 6526, 10: 58146, 20: 35328}, + ("Category Fives", 5, 4): {0: 2615, 10: 44108, 15: 32247, 20: 21030}, + ("Category Fives", 5, 5): {0: 1063, 10: 31079, 15: 34489, 25: 33369}, + ("Category Fives", 5, 6): {0: 4520, 15: 49551, 20: 32891, 25: 13038}, + ("Category Fives", 5, 7): {0: 2370, 15: 40714, 20: 37778, 25: 19138}, + ("Category Fives", 5, 8): {0: 1179, 15: 31909, 20: 40615, 25: 26297}, ("Category Fives", 6, 0): {0: 100000}, - ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 20181, 15: 6176}, - ("Category Fives", 6, 2): {0: 11322, 5: 29613, 10: 32664, 15: 19004, 20: 7397}, - ("Category Fives", 6, 3): {0: 3765, 5: 16288, 10: 29770, 15: 29233, 20: 15759, 25: 5185}, - ("Category Fives", 6, 4): {0: 1201, 5: 8226, 10: 21518, 15: 31229, 20: 25160, 25: 12666}, - ("Category Fives", 6, 5): {0: 433, 10: 17879, 15: 27961, 20: 30800, 25: 18442, 30: 4485}, - ("Category Fives", 6, 6): {0: 141, 10: 10040, 15: 22226, 20: 32744, 25: 26341, 30: 8508}, - ("Category Fives", 6, 7): {0: 772, 10: 4724, 15: 16206, 20: 31363, 25: 32784, 30: 14151}, - ("Category Fives", 6, 8): {0: 297, 15: 13902, 20: 28004, 25: 37178, 30: 20619}, + ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 26357}, + ("Category Fives", 6, 2): {0: 11322, 10: 62277, 20: 26401}, + ("Category Fives", 6, 3): {0: 3765, 10: 46058, 20: 50177}, + ("Category Fives", 6, 4): {0: 1201, 15: 60973, 25: 37826}, + ("Category Fives", 6, 5): {0: 4307, 15: 41966, 20: 30800, 25: 22927}, + ("Category Fives", 6, 6): {0: 1827, 15: 30580, 20: 32744, 30: 34849}, + ("Category Fives", 6, 7): {0: 5496, 20: 47569, 25: 32784, 30: 14151}, + ("Category Fives", 6, 8): {0: 2920, 20: 39283, 25: 37178, 30: 20619}, ("Category Fives", 7, 0): {0: 100000}, - ("Category Fives", 7, 1): {0: 27826, 5: 39154, 10: 23567, 15: 9453}, - ("Category Fives", 7, 2): {0: 7609, 5: 24193, 10: 31722, 15: 23214, 20: 10140, 25: 3122}, - ("Category Fives", 7, 3): {0: 2262, 5: 11013, 10: 24443, 15: 29307, 20: 21387, 25: 11588}, - ("Category Fives", 7, 4): {0: 618, 5: 4583, 10: 14761, 15: 26159, 20: 28335, 25: 18050, 30: 7494}, - ("Category Fives", 7, 5): {0: 183, 10: 9616, 15: 19685, 20: 28915, 25: 26000, 30: 12883, 35: 2718}, - ("Category Fives", 7, 6): {0: 670, 15: 16878, 20: 25572, 25: 30456, 30: 20695, 35: 5729}, - ("Category Fives", 7, 7): {0: 255, 15: 9718, 20: 20696, 25: 31883, 30: 27333, 35: 10115}, - ("Category Fives", 7, 8): {0: 927, 15: 4700, 20: 15292, 25: 30298, 30: 33015, 35: 15768}, + ("Category Fives", 7, 1): {0: 27826, 5: 39154, 15: 33020}, + ("Category Fives", 7, 2): {0: 7609, 10: 55915, 20: 36476}, + ("Category Fives", 7, 3): {0: 2262, 10: 35456, 20: 62282}, + ("Category Fives", 7, 4): {0: 5201, 15: 40920, 25: 53879}, + ("Category Fives", 7, 5): {0: 1890, 20: 56509, 30: 41601}, + ("Category Fives", 7, 6): {0: 4506, 20: 38614, 25: 30456, 30: 26424}, + ("Category Fives", 7, 7): {0: 2107, 25: 60445, 35: 37448}, + ("Category Fives", 7, 8): {0: 5627, 25: 45590, 30: 33015, 35: 15768}, ("Category Fives", 8, 0): {0: 100000}, - ("Category Fives", 8, 1): {0: 23333, 5: 37259, 10: 25947, 15: 10392, 20: 3069}, - ("Category Fives", 8, 2): {0: 5425, 5: 18915, 10: 29380, 15: 25994, 20: 14056, 25: 6230}, - ("Category Fives", 8, 3): {0: 1258, 5: 7317, 10: 18783, 15: 27375, 20: 24542, 25: 14322, 30: 6403}, - ("Category Fives", 8, 4): {0: 271, 10: 11864, 15: 20267, 20: 27158, 25: 23589, 30: 12529, 35: 4322}, - ("Category Fives", 8, 5): {0: 943, 10: 4260, 15: 12456, 20: 23115, 25: 27968, 30: 20704, 35: 10554}, - ("Category Fives", 8, 6): {0: 281, 15: 8625, 20: 17201, 25: 27484, 30: 27178, 35: 15414, 40: 3817}, - ("Category Fives", 8, 7): {0: 746, 20: 14964, 25: 23717, 30: 30426, 35: 22677, 40: 7470}, - ("Category Fives", 8, 8): {0: 261, 20: 8927, 25: 18714, 30: 31084, 35: 29126, 40: 11888}, + ("Category Fives", 8, 1): {0: 23333, 5: 37259, 15: 39408}, + ("Category Fives", 8, 2): {0: 5425, 10: 48295, 20: 46280}, + ("Category Fives", 8, 3): {0: 1258, 15: 53475, 25: 45267}, + ("Category Fives", 8, 4): {0: 2752, 20: 56808, 30: 40440}, + ("Category Fives", 8, 5): {0: 5203, 20: 35571, 30: 59226}, + ("Category Fives", 8, 6): {0: 1970, 25: 51621, 35: 46409}, + ("Category Fives", 8, 7): {0: 4281, 25: 35146, 30: 30426, 40: 30147}, + ("Category Fives", 8, 8): {0: 2040, 30: 56946, 40: 41014}, ("Category Sixes", 0, 0): {0: 100000}, ("Category Sixes", 0, 1): {0: 100000}, ("Category Sixes", 0, 2): {0: 100000}, @@ -431,8 +431,8 @@ ("Category Sixes", 1, 7): {0: 28251, 6: 71749}, ("Category Sixes", 1, 8): {0: 23206, 6: 76794}, ("Category Sixes", 2, 0): {0: 100000}, - ("Category Sixes", 2, 1): {0: 69463, 6: 27651, 12: 2886}, - ("Category Sixes", 2, 2): {0: 47896, 6: 42794, 12: 9310}, + ("Category Sixes", 2, 1): {0: 69463, 6: 30537}, + ("Category Sixes", 2, 2): {0: 47896, 6: 52104}, ("Category Sixes", 2, 3): {0: 33394, 6: 48757, 12: 17849}, ("Category Sixes", 2, 4): {0: 23552, 6: 49554, 12: 26894}, ("Category Sixes", 2, 5): {0: 16090, 6: 48098, 12: 35812}, @@ -440,59 +440,59 @@ ("Category Sixes", 2, 7): {0: 7737, 6: 40480, 12: 51783}, ("Category Sixes", 2, 8): {0: 5379, 6: 35672, 12: 58949}, ("Category Sixes", 3, 0): {0: 100000}, - ("Category Sixes", 3, 1): {0: 57718, 6: 34818, 12: 7464}, - ("Category Sixes", 3, 2): {0: 33610, 6: 44328, 12: 19159, 18: 2903}, - ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 30952, 18: 7436}, + ("Category Sixes", 3, 1): {0: 57718, 6: 42282}, + ("Category Sixes", 3, 2): {0: 33610, 6: 44328, 12: 22062}, + ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 38388}, ("Category Sixes", 3, 4): {0: 11144, 6: 36281, 12: 38817, 18: 13758}, ("Category Sixes", 3, 5): {0: 6414, 6: 28891, 12: 43114, 18: 21581}, - ("Category Sixes", 3, 6): {0: 3870, 6: 22394, 12: 44318, 18: 29418}, - ("Category Sixes", 3, 7): {0: 2188, 6: 16803, 12: 43487, 18: 37522}, - ("Category Sixes", 3, 8): {0: 1289, 6: 12421, 12: 41082, 18: 45208}, + ("Category Sixes", 3, 6): {0: 3870, 12: 66712, 18: 29418}, + ("Category Sixes", 3, 7): {0: 2188, 12: 60290, 18: 37522}, + ("Category Sixes", 3, 8): {0: 1289, 12: 53503, 18: 45208}, ("Category Sixes", 4, 0): {0: 100000}, ("Category Sixes", 4, 1): {0: 48197, 6: 38521, 12: 13282}, - ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 26935, 18: 8731}, - ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 17390, 24: 3157}, - ("Category Sixes", 4, 4): {0: 5324, 6: 23265, 12: 37209, 18: 26929, 24: 7273}, - ("Category Sixes", 4, 5): {0: 2658, 6: 15488, 12: 34685, 18: 34476, 24: 12693}, - ("Category Sixes", 4, 6): {0: 1282, 6: 9997, 12: 29855, 18: 39379, 24: 19487}, - ("Category Sixes", 4, 7): {0: 588, 6: 6202, 12: 24396, 18: 41935, 24: 26879}, - ("Category Sixes", 4, 8): {0: 317, 6: 3863, 12: 19042, 18: 42180, 24: 34598}, + ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 35666}, + ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 20547}, + ("Category Sixes", 4, 4): {0: 5324, 12: 60474, 18: 34202}, + ("Category Sixes", 4, 5): {0: 2658, 12: 50173, 18: 34476, 24: 12693}, + ("Category Sixes", 4, 6): {0: 1282, 12: 39852, 18: 39379, 24: 19487}, + ("Category Sixes", 4, 7): {0: 588, 12: 30598, 18: 41935, 24: 26879}, + ("Category Sixes", 4, 8): {0: 4180, 18: 61222, 24: 34598}, ("Category Sixes", 5, 0): {0: 100000}, - ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 16206, 18: 3497}, - ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 13612, 24: 3281}, - ("Category Sixes", 5, 3): {0: 6456, 6: 23539, 12: 34585, 18: 25020, 24: 10400}, - ("Category Sixes", 5, 4): {0: 2581, 6: 13980, 12: 30355, 18: 32198, 24: 17115, 30: 3771}, - ("Category Sixes", 5, 5): {0: 1119, 6: 7775, 12: 23063, 18: 34716, 24: 25568, 30: 7759}, - ("Category Sixes", 5, 6): {0: 392, 6: 4171, 12: 16724, 18: 32792, 24: 32829, 30: 13092}, - ("Category Sixes", 5, 7): {0: 197, 12: 13627, 18: 29190, 24: 37560, 30: 19426}, - ("Category Sixes", 5, 8): {0: 1246, 12: 7404, 18: 24560, 24: 40134, 30: 26656}, + ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 19703}, + ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 16893}, + ("Category Sixes", 5, 3): {0: 6456, 12: 58124, 18: 25020, 24: 10400}, + ("Category Sixes", 5, 4): {0: 2581, 12: 44335, 18: 32198, 24: 20886}, + ("Category Sixes", 5, 5): {0: 1119, 12: 30838, 18: 34716, 24: 33327}, + ("Category Sixes", 5, 6): {0: 4563, 18: 49516, 24: 32829, 30: 13092}, + ("Category Sixes", 5, 7): {0: 2315, 18: 40699, 24: 37560, 30: 19426}, + ("Category Sixes", 5, 8): {0: 1246, 18: 31964, 24: 40134, 30: 26656}, ("Category Sixes", 6, 0): {0: 100000}, - ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 12: 20198, 18: 6268}, - ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 18: 19196, 24: 7514}, - ("Category Sixes", 6, 3): {0: 3787, 6: 16266, 12: 29873, 18: 29107, 24: 15863, 30: 5104}, - ("Category Sixes", 6, 4): {0: 1286, 6: 8066, 12: 21653, 18: 31264, 24: 25039, 30: 12692}, - ("Category Sixes", 6, 5): {0: 413, 6: 3777, 12: 13962, 18: 27705, 24: 30919, 30: 18670, 36: 4554}, - ("Category Sixes", 6, 6): {0: 146, 12: 10040, 18: 22320, 24: 32923, 30: 26086, 36: 8485}, - ("Category Sixes", 6, 7): {0: 814, 12: 4698, 18: 16286, 24: 31577, 30: 32487, 36: 14138}, - ("Category Sixes", 6, 8): {0: 328, 18: 14004, 24: 28064, 30: 37212, 36: 20392}, + ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 18: 26466}, + ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 24: 26710}, + ("Category Sixes", 6, 3): {0: 3787, 12: 46139, 18: 29107, 24: 20967}, + ("Category Sixes", 6, 4): {0: 1286, 12: 29719, 18: 31264, 24: 25039, 30: 12692}, + ("Category Sixes", 6, 5): {0: 4190, 18: 41667, 24: 30919, 30: 23224}, + ("Category Sixes", 6, 6): {0: 1804, 18: 30702, 24: 32923, 30: 34571}, + ("Category Sixes", 6, 7): {0: 51, 24: 53324, 30: 32487, 36: 14138}, + ("Category Sixes", 6, 8): {0: 2886, 24: 39510, 30: 37212, 36: 20392}, ("Category Sixes", 7, 0): {0: 100000}, - ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 12: 23499, 18: 9665}, - ("Category Sixes", 7, 2): {0: 7883, 6: 23846, 12: 31558, 18: 23295, 24: 10316, 30: 3102}, - ("Category Sixes", 7, 3): {0: 2186, 6: 10928, 12: 24321, 18: 29650, 24: 21177, 30: 9209, 36: 2529}, - ("Category Sixes", 7, 4): {0: 603, 6: 4459, 12: 14673, 18: 26303, 24: 28335, 30: 18228, 36: 7399}, - ("Category Sixes", 7, 5): {0: 172, 12: 9654, 18: 19381, 24: 29254, 30: 25790, 36: 12992, 42: 2757}, - ("Category Sixes", 7, 6): {0: 704, 12: 3864, 18: 13039, 24: 25760, 30: 30698, 36: 20143, 42: 5792}, - ("Category Sixes", 7, 7): {0: 257, 18: 9857, 24: 20557, 30: 31709, 36: 27546, 42: 10074}, - ("Category Sixes", 7, 8): {0: 872, 18: 4658, 24: 15419, 30: 30259, 36: 33183, 42: 15609}, + ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 18: 33164}, + ("Category Sixes", 7, 2): {0: 7883, 12: 55404, 24: 36713}, + ("Category Sixes", 7, 3): {0: 2186, 12: 35249, 18: 29650, 30: 32915}, + ("Category Sixes", 7, 4): {0: 5062, 18: 40976, 24: 28335, 36: 25627}, + ("Category Sixes", 7, 5): {0: 1947, 18: 27260, 24: 29254, 30: 25790, 36: 15749}, + ("Category Sixes", 7, 6): {0: 4568, 24: 38799, 30: 30698, 42: 25935}, + ("Category Sixes", 7, 7): {0: 2081, 24: 28590, 30: 31709, 36: 37620}, + ("Category Sixes", 7, 8): {0: 73, 30: 51135, 36: 33183, 42: 15609}, ("Category Sixes", 8, 0): {0: 100000}, - ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 10483, 24: 3123}, - ("Category Sixes", 8, 2): {0: 5280, 6: 18943, 12: 29664, 18: 25777, 24: 14170, 30: 6166}, - ("Category Sixes", 8, 3): {0: 1246, 6: 7112, 12: 18757, 18: 27277, 24: 24802, 30: 14351, 36: 6455}, - ("Category Sixes", 8, 4): {0: 301, 12: 12044, 18: 20247, 24: 27146, 30: 23403, 36: 12524, 42: 4335}, - ("Category Sixes", 8, 5): {0: 859, 12: 4241, 18: 12477, 24: 23471, 30: 27655, 36: 20803, 42: 10494}, - ("Category Sixes", 8, 6): {0: 277, 18: 8656, 24: 17373, 30: 27347, 36: 27024, 42: 15394, 48: 3929}, - ("Category Sixes", 8, 7): {0: 766, 18: 3503, 24: 11451, 30: 23581, 36: 30772, 42: 22654, 48: 7273}, - ("Category Sixes", 8, 8): {6: 262, 24: 8866, 30: 18755, 36: 31116, 42: 28870, 48: 12131}, + ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 13606}, + ("Category Sixes", 8, 2): {0: 5280, 12: 48607, 18: 25777, 30: 20336}, + ("Category Sixes", 8, 3): {0: 1246, 12: 25869, 18: 27277, 30: 45608}, + ("Category Sixes", 8, 4): {0: 2761, 18: 29831, 24: 27146, 36: 40262}, + ("Category Sixes", 8, 5): {0: 5100, 24: 35948, 30: 27655, 42: 31297}, + ("Category Sixes", 8, 6): {0: 2067, 30: 51586, 36: 27024, 42: 19323}, + ("Category Sixes", 8, 7): {0: 4269, 30: 35032, 36: 30772, 48: 29927}, + ("Category Sixes", 8, 8): {6: 2012, 30: 25871, 36: 31116, 42: 28870, 48: 12131}, ("Category Choice", 0, 0): {0: 100000}, ("Category Choice", 0, 1): {0: 100000}, ("Category Choice", 0, 2): {0: 100000}, @@ -503,77 +503,77 @@ ("Category Choice", 0, 7): {0: 100000}, ("Category Choice", 0, 8): {0: 100000}, ("Category Choice", 1, 0): {0: 100000}, - ("Category Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, - ("Category Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, - ("Category Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, - ("Category Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, - ("Category Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, - ("Category Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, - ("Category Choice", 1, 7): {1: 8893, 6: 91107}, - ("Category Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Choice", 1, 1): {1: 33315, 5: 66685}, + ("Category Choice", 1, 2): {1: 10921, 5: 89079}, + ("Category Choice", 1, 3): {1: 27995, 6: 72005}, + ("Category Choice", 1, 4): {1: 15490, 6: 84510}, + ("Category Choice", 1, 5): {1: 6390, 6: 93610}, + ("Category Choice", 1, 6): {1: 34656, 6: 65344}, + ("Category Choice", 1, 7): {1: 28829, 6: 71171}, + ("Category Choice", 1, 8): {1: 23996, 6: 76004}, ("Category Choice", 2, 0): {0: 100000}, - ("Category Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, - ("Category Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, - ("Category Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, - ("Category Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, - ("Category Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, - ("Category Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, - ("Category Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, - ("Category Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Choice", 2, 1): {2: 16796, 8: 83204}, + ("Category Choice", 2, 2): {2: 22212, 10: 77788}, + ("Category Choice", 2, 3): {2: 29002, 11: 70998}, + ("Category Choice", 2, 4): {2: 22485, 11: 77515}, + ("Category Choice", 2, 5): {2: 28019, 12: 71981}, + ("Category Choice", 2, 6): {2: 23193, 12: 76807}, + ("Category Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, + ("Category Choice", 2, 8): {2: 9297, 12: 90703}, ("Category Choice", 3, 0): {0: 100000}, - ("Category Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, - ("Category Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, - ("Category Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, - ("Category Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, - ("Category Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, - ("Category Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, - ("Category Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, - ("Category Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Choice", 3, 1): {3: 25983, 12: 74017}, + ("Category Choice", 3, 2): {3: 24419, 14: 75581}, + ("Category Choice", 3, 3): {3: 24466, 15: 75534}, + ("Category Choice", 3, 4): {3: 25866, 16: 74134}, + ("Category Choice", 3, 5): {3: 30994, 17: 69006}, + ("Category Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, + ("Category Choice", 3, 7): {3: 28667, 18: 71333}, + ("Category Choice", 3, 8): {3: 23852, 18: 76148}, ("Category Choice", 4, 0): {0: 100000}, - ("Category Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, - ("Category Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, - ("Category Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, - ("Category Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, - ("Category Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, - ("Category Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, - ("Category Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, - ("Category Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, + ("Category Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, + ("Category Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, + ("Category Choice", 4, 4): {4: 30873, 21: 69127}, + ("Category Choice", 4, 5): {4: 31056, 22: 68944}, + ("Category Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, + ("Category Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, + ("Category Choice", 4, 8): {5: 31948, 24: 68052}, ("Category Choice", 5, 0): {0: 100000}, - ("Category Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, - ("Category Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, - ("Category Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, - ("Category Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, - ("Category Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, - ("Category Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, - ("Category Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, - ("Category Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, + ("Category Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, + ("Category Choice", 5, 3): {6: 22251, 24: 77749}, + ("Category Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, + ("Category Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, + ("Category Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, + ("Category Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, + ("Category Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, ("Category Choice", 6, 0): {0: 100000}, - ("Category Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, - ("Category Choice", 6, 2): {8: 1504, 16: 8676, 20: 10032, 22: 14673, 26: 27312, 27: 16609, 29: 12133, 31: 9061}, - ("Category Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, - ("Category Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, - ("Category Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, - ("Category Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, - ("Category Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, - ("Category Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Choice", 6, 1): {6: 27848, 23: 72152}, + ("Category Choice", 6, 2): {8: 27078, 27: 72922}, + ("Category Choice", 6, 3): {6: 27876, 29: 72124}, + ("Category Choice", 6, 4): {9: 30912, 31: 69088}, + ("Category Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, + ("Category Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, + ("Category Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, + ("Category Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, ("Category Choice", 7, 0): {0: 100000}, - ("Category Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, - ("Category Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, - ("Category Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, - ("Category Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, - ("Category Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, - ("Category Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, - ("Category Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, - ("Category Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, + ("Category Choice", 7, 2): {10: 27324, 31: 72676}, + ("Category Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, + ("Category Choice", 7, 4): {13: 26663, 35: 73337}, + ("Category Choice", 7, 5): {12: 29276, 37: 70724}, + ("Category Choice", 7, 6): {14: 26539, 38: 73461}, + ("Category Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, + ("Category Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, ("Category Choice", 8, 0): {0: 100000}, - ("Category Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, - ("Category Choice", 8, 2): {11: 5900, 26: 10331, 29: 11435, 31: 14533, 34: 23939, 36: 13855, 38: 10165, 40: 9842}, - ("Category Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, - ("Category Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, - ("Category Choice", 8, 5): {16: 4986, 32: 9031, 35: 10214, 37: 14528, 41: 25608, 42: 16131, 44: 11245, 46: 8257}, - ("Category Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, - ("Category Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, - ("Category Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, + ("Category Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, + ("Category Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, + ("Category Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, + ("Category Choice", 8, 5): {16: 30949, 42: 69051}, + ("Category Choice", 8, 6): {16: 26968, 43: 73032}, + ("Category Choice", 8, 7): {20: 24559, 44: 75441}, + ("Category Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, ("Category Inverse Choice", 0, 0): {0: 100000}, ("Category Inverse Choice", 0, 1): {0: 100000}, ("Category Inverse Choice", 0, 2): {0: 100000}, @@ -584,104 +584,77 @@ ("Category Inverse Choice", 0, 7): {0: 100000}, ("Category Inverse Choice", 0, 8): {0: 100000}, ("Category Inverse Choice", 1, 0): {0: 100000}, - ("Category Inverse Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, - ("Category Inverse Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, - ("Category Inverse Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, - ("Category Inverse Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, - ("Category Inverse Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, - ("Category Inverse Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, - ("Category Inverse Choice", 1, 7): {1: 8893, 6: 91107}, - ("Category Inverse Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Inverse Choice", 1, 1): {1: 33315, 5: 66685}, + ("Category Inverse Choice", 1, 2): {1: 10921, 5: 89079}, + ("Category Inverse Choice", 1, 3): {1: 27995, 6: 72005}, + ("Category Inverse Choice", 1, 4): {1: 15490, 6: 84510}, + ("Category Inverse Choice", 1, 5): {1: 6390, 6: 93610}, + ("Category Inverse Choice", 1, 6): {1: 34656, 6: 65344}, + ("Category Inverse Choice", 1, 7): {1: 28829, 6: 71171}, + ("Category Inverse Choice", 1, 8): {1: 23996, 6: 76004}, ("Category Inverse Choice", 2, 0): {0: 100000}, - ("Category Inverse Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, - ("Category Inverse Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, - ("Category Inverse Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, - ("Category Inverse Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, - ("Category Inverse Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, - ("Category Inverse Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, - ("Category Inverse Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, - ("Category Inverse Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Inverse Choice", 2, 1): {2: 16796, 8: 83204}, + ("Category Inverse Choice", 2, 2): {2: 22212, 10: 77788}, + ("Category Inverse Choice", 2, 3): {2: 29002, 11: 70998}, + ("Category Inverse Choice", 2, 4): {2: 22485, 11: 77515}, + ("Category Inverse Choice", 2, 5): {2: 28019, 12: 71981}, + ("Category Inverse Choice", 2, 6): {2: 23193, 12: 76807}, + ("Category Inverse Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, + ("Category Inverse Choice", 2, 8): {2: 9297, 12: 90703}, ("Category Inverse Choice", 3, 0): {0: 100000}, - ("Category Inverse Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, - ("Category Inverse Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, - ("Category Inverse Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, - ("Category Inverse Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, - ("Category Inverse Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, - ("Category Inverse Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, - ("Category Inverse Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, - ("Category Inverse Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Inverse Choice", 3, 1): {3: 25983, 12: 74017}, + ("Category Inverse Choice", 3, 2): {3: 24419, 14: 75581}, + ("Category Inverse Choice", 3, 3): {3: 24466, 15: 75534}, + ("Category Inverse Choice", 3, 4): {3: 25866, 16: 74134}, + ("Category Inverse Choice", 3, 5): {3: 30994, 17: 69006}, + ("Category Inverse Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, + ("Category Inverse Choice", 3, 7): {3: 28667, 18: 71333}, + ("Category Inverse Choice", 3, 8): {3: 23852, 18: 76148}, ("Category Inverse Choice", 4, 0): {0: 100000}, - ("Category Inverse Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, - ("Category Inverse Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, - ("Category Inverse Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, - ("Category Inverse Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, - ("Category Inverse Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, - ("Category Inverse Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, - ("Category Inverse Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, - ("Category Inverse Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Inverse Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, + ("Category Inverse Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, + ("Category Inverse Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, + ("Category Inverse Choice", 4, 4): {4: 30873, 21: 69127}, + ("Category Inverse Choice", 4, 5): {4: 31056, 22: 68944}, + ("Category Inverse Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, + ("Category Inverse Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, + ("Category Inverse Choice", 4, 8): {5: 31948, 24: 68052}, ("Category Inverse Choice", 5, 0): {0: 100000}, - ("Category Inverse Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, - ("Category Inverse Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, - ("Category Inverse Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, - ("Category Inverse Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, - ("Category Inverse Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, - ("Category Inverse Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, - ("Category Inverse Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, - ("Category Inverse Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Inverse Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, + ("Category Inverse Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, + ("Category Inverse Choice", 5, 3): {6: 22251, 24: 77749}, + ("Category Inverse Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, + ("Category Inverse Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, + ("Category Inverse Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, + ("Category Inverse Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, + ("Category Inverse Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, ("Category Inverse Choice", 6, 0): {0: 100000}, - ("Category Inverse Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, - ("Category Inverse Choice", 6, 2): { - 8: 1504, - 16: 8676, - 20: 10032, - 22: 14673, - 26: 27312, - 27: 16609, - 29: 12133, - 31: 9061, - }, - ("Category Inverse Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, - ("Category Inverse Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, - ("Category Inverse Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, - ("Category Inverse Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, - ("Category Inverse Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, - ("Category Inverse Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Inverse Choice", 6, 1): {6: 27848, 23: 72152}, + ("Category Inverse Choice", 6, 2): {8: 27078, 27: 72922}, + ("Category Inverse Choice", 6, 3): {6: 27876, 29: 72124}, + ("Category Inverse Choice", 6, 4): {9: 30912, 31: 69088}, + ("Category Inverse Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, + ("Category Inverse Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, + ("Category Inverse Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, + ("Category Inverse Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, ("Category Inverse Choice", 7, 0): {0: 100000}, - ("Category Inverse Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, - ("Category Inverse Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, - ("Category Inverse Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, - ("Category Inverse Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, - ("Category Inverse Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, - ("Category Inverse Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, - ("Category Inverse Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, - ("Category Inverse Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Inverse Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, + ("Category Inverse Choice", 7, 2): {10: 27324, 31: 72676}, + ("Category Inverse Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, + ("Category Inverse Choice", 7, 4): {13: 26663, 35: 73337}, + ("Category Inverse Choice", 7, 5): {12: 29276, 37: 70724}, + ("Category Inverse Choice", 7, 6): {14: 26539, 38: 73461}, + ("Category Inverse Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, + ("Category Inverse Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, ("Category Inverse Choice", 8, 0): {0: 100000}, - ("Category Inverse Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, - ("Category Inverse Choice", 8, 2): { - 11: 5900, - 26: 10331, - 29: 11435, - 31: 14533, - 34: 23939, - 36: 13855, - 38: 10165, - 40: 9842, - }, - ("Category Inverse Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, - ("Category Inverse Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, - ("Category Inverse Choice", 8, 5): { - 16: 4986, - 32: 9031, - 35: 10214, - 37: 14528, - 41: 25608, - 42: 16131, - 44: 11245, - 46: 8257, - }, - ("Category Inverse Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, - ("Category Inverse Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, - ("Category Inverse Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Inverse Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, + ("Category Inverse Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, + ("Category Inverse Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, + ("Category Inverse Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, + ("Category Inverse Choice", 8, 5): {16: 30949, 42: 69051}, + ("Category Inverse Choice", 8, 6): {16: 26968, 43: 73032}, + ("Category Inverse Choice", 8, 7): {20: 24559, 44: 75441}, + ("Category Inverse Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, ("Category Pair", 0, 0): {0: 100000}, ("Category Pair", 0, 1): {0: 100000}, ("Category Pair", 0, 2): {0: 100000}, @@ -791,7 +764,7 @@ ("Category Three of a Kind", 2, 7): {0: 100000}, ("Category Three of a Kind", 2, 8): {0: 100000}, ("Category Three of a Kind", 3, 0): {0: 100000}, - ("Category Three of a Kind", 3, 1): {0: 97222, 20: 2778}, + ("Category Three of a Kind", 3, 1): {0: 100000}, ("Category Three of a Kind", 3, 2): {0: 88880, 20: 11120}, ("Category Three of a Kind", 3, 3): {0: 78187, 20: 21813}, ("Category Three of a Kind", 3, 4): {0: 67476, 20: 32524}, @@ -881,7 +854,7 @@ ("Category Four of a Kind", 3, 7): {0: 100000}, ("Category Four of a Kind", 3, 8): {0: 100000}, ("Category Four of a Kind", 4, 0): {0: 100000}, - ("Category Four of a Kind", 4, 1): {0: 99516, 30: 484}, + ("Category Four of a Kind", 4, 1): {0: 100000}, ("Category Four of a Kind", 4, 2): {0: 96122, 30: 3878}, ("Category Four of a Kind", 4, 3): {0: 89867, 30: 10133}, ("Category Four of a Kind", 4, 4): {0: 81771, 30: 18229}, @@ -1304,7 +1277,7 @@ ("Category Yacht", 5, 7): {0: 67007, 50: 32993}, ("Category Yacht", 5, 8): {0: 58618, 50: 41382}, ("Category Yacht", 6, 0): {0: 100000}, - ("Category Yacht", 6, 1): {0: 99571, 50: 429}, + ("Category Yacht", 6, 1): {0: 100000}, ("Category Yacht", 6, 2): {0: 94726, 50: 5274}, ("Category Yacht", 6, 3): {0: 84366, 50: 15634}, ("Category Yacht", 6, 4): {0: 70782, 50: 29218}, @@ -1313,7 +1286,7 @@ ("Category Yacht", 6, 7): {0: 33578, 50: 66422}, ("Category Yacht", 6, 8): {0: 25079, 50: 74921}, ("Category Yacht", 7, 0): {0: 100000}, - ("Category Yacht", 7, 1): {0: 98833, 50: 1167}, + ("Category Yacht", 7, 1): {0: 100000}, ("Category Yacht", 7, 2): {0: 87511, 50: 12489}, ("Category Yacht", 7, 3): {0: 68252, 50: 31748}, ("Category Yacht", 7, 4): {0: 49065, 50: 50935}, @@ -1346,51 +1319,51 @@ ("Category Distincts", 2, 6): {1: 1, 2: 99999}, ("Category Distincts", 2, 7): {2: 100000}, ("Category Distincts", 2, 8): {2: 100000}, - ("Category Distincts", 3, 1): {1: 2760, 2: 41714, 3: 55526}, - ("Category Distincts", 3, 2): {1: 78, 3: 99922}, + ("Category Distincts", 3, 1): {1: 2760, 3: 97240}, + ("Category Distincts", 3, 2): {1: 15014, 3: 84986}, ("Category Distincts", 3, 3): {1: 4866, 3: 95134}, ("Category Distincts", 3, 4): {2: 1659, 3: 98341}, ("Category Distincts", 3, 5): {2: 575, 3: 99425}, ("Category Distincts", 3, 6): {2: 200, 3: 99800}, ("Category Distincts", 3, 7): {2: 69, 3: 99931}, ("Category Distincts", 3, 8): {2: 22, 3: 99978}, - ("Category Distincts", 4, 1): {1: 494, 3: 71611, 4: 27895}, - ("Category Distincts", 4, 2): {1: 1893, 3: 36922, 4: 61185}, - ("Category Distincts", 4, 3): {2: 230, 4: 99770}, - ("Category Distincts", 4, 4): {2: 21, 4: 99979}, + ("Category Distincts", 4, 1): {1: 16634, 3: 83366}, + ("Category Distincts", 4, 2): {1: 1893, 4: 98107}, + ("Category Distincts", 4, 3): {2: 19861, 4: 80139}, + ("Category Distincts", 4, 4): {2: 9879, 4: 90121}, ("Category Distincts", 4, 5): {2: 4906, 4: 95094}, ("Category Distincts", 4, 6): {3: 2494, 4: 97506}, ("Category Distincts", 4, 7): {3: 1297, 4: 98703}, ("Category Distincts", 4, 8): {3: 611, 4: 99389}, - ("Category Distincts", 5, 1): {1: 5798, 3: 38538, 4: 55664}, - ("Category Distincts", 5, 2): {2: 196, 4: 68119, 5: 31685}, - ("Category Distincts", 5, 3): {2: 3022, 4: 44724, 5: 52254}, - ("Category Distincts", 5, 4): {3: 722, 4: 31632, 5: 67646}, - ("Category Distincts", 5, 5): {3: 215, 4: 21391, 5: 78394}, - ("Category Distincts", 5, 6): {3: 55, 5: 99945}, - ("Category Distincts", 5, 7): {3: 15, 5: 99985}, + ("Category Distincts", 5, 1): {1: 5798, 4: 94202}, + ("Category Distincts", 5, 2): {2: 11843, 4: 88157}, + ("Category Distincts", 5, 3): {2: 3022, 5: 96978}, + ("Category Distincts", 5, 4): {3: 32354, 5: 67646}, + ("Category Distincts", 5, 5): {3: 21606, 5: 78394}, + ("Category Distincts", 5, 6): {3: 14525, 5: 85475}, + ("Category Distincts", 5, 7): {3: 9660, 5: 90340}, ("Category Distincts", 5, 8): {3: 6463, 5: 93537}, - ("Category Distincts", 6, 1): {1: 2027, 3: 22985, 4: 50464, 5: 24524}, - ("Category Distincts", 6, 2): {2: 3299, 4: 35174, 5: 61527}, - ("Category Distincts", 6, 3): {3: 417, 5: 79954, 6: 19629}, - ("Category Distincts", 6, 4): {3: 7831, 5: 61029, 6: 31140}, - ("Category Distincts", 6, 5): {3: 3699, 5: 54997, 6: 41304}, - ("Category Distincts", 6, 6): {4: 1557, 5: 47225, 6: 51218}, - ("Category Distincts", 6, 7): {4: 728, 5: 40465, 6: 58807}, - ("Category Distincts", 6, 8): {4: 321, 5: 33851, 6: 65828}, - ("Category Distincts", 7, 1): {1: 665, 4: 57970, 5: 41365}, - ("Category Distincts", 7, 2): {2: 839, 5: 75578, 6: 23583}, - ("Category Distincts", 7, 3): {3: 6051, 5: 50312, 6: 43637}, - ("Category Distincts", 7, 4): {3: 1796, 5: 38393, 6: 59811}, - ("Category Distincts", 7, 5): {4: 529, 5: 27728, 6: 71743}, - ("Category Distincts", 7, 6): {4: 164, 6: 99836}, - ("Category Distincts", 7, 7): {4: 53, 6: 99947}, - ("Category Distincts", 7, 8): {4: 14, 6: 99986}, - ("Category Distincts", 8, 1): {1: 7137, 4: 36582, 5: 56281}, - ("Category Distincts", 8, 2): {2: 233, 5: 59964, 6: 39803}, - ("Category Distincts", 8, 3): {3: 1976, 5: 34748, 6: 63276}, - ("Category Distincts", 8, 4): {4: 389, 5: 21008, 6: 78603}, - ("Category Distincts", 8, 5): {4: 78, 6: 99922}, + ("Category Distincts", 6, 1): {1: 25012, 4: 74988}, + ("Category Distincts", 6, 2): {2: 3299, 5: 96701}, + ("Category Distincts", 6, 3): {3: 17793, 5: 82207}, + ("Category Distincts", 6, 4): {3: 7831, 5: 92169}, + ("Category Distincts", 6, 5): {3: 3699, 6: 96301}, + ("Category Distincts", 6, 6): {4: 1557, 6: 98443}, + ("Category Distincts", 6, 7): {4: 728, 6: 99272}, + ("Category Distincts", 6, 8): {4: 321, 6: 99679}, + ("Category Distincts", 7, 1): {1: 13671, 5: 86329}, + ("Category Distincts", 7, 2): {2: 19686, 5: 80314}, + ("Category Distincts", 7, 3): {3: 6051, 6: 93949}, + ("Category Distincts", 7, 4): {3: 1796, 6: 98204}, + ("Category Distincts", 7, 5): {4: 28257, 6: 71743}, + ("Category Distincts", 7, 6): {4: 19581, 6: 80419}, + ("Category Distincts", 7, 7): {4: 13618, 6: 86382}, + ("Category Distincts", 7, 8): {4: 9545, 6: 90455}, + ("Category Distincts", 8, 1): {1: 7137, 5: 92863}, + ("Category Distincts", 8, 2): {2: 9414, 6: 90586}, + ("Category Distincts", 8, 3): {3: 1976, 6: 98024}, + ("Category Distincts", 8, 4): {4: 21397, 6: 78603}, + ("Category Distincts", 8, 5): {4: 12592, 6: 87408}, ("Category Distincts", 8, 6): {4: 7177, 6: 92823}, ("Category Distincts", 8, 7): {4: 4179, 6: 95821}, ("Category Distincts", 8, 8): {5: 2440, 6: 97560}, @@ -1404,8 +1377,8 @@ ("Category Two times Ones", 0, 7): {0: 100000}, ("Category Two times Ones", 0, 8): {0: 100000}, ("Category Two times Ones", 1, 0): {0: 100000}, - ("Category Two times Ones", 1, 1): {0: 83475, 2: 16525}, - ("Category Two times Ones", 1, 2): {0: 69690, 2: 30310}, + ("Category Two times Ones", 1, 1): {0: 100000}, + ("Category Two times Ones", 1, 2): {0: 100000}, ("Category Two times Ones", 1, 3): {0: 57818, 2: 42182}, ("Category Two times Ones", 1, 4): {0: 48418, 2: 51582}, ("Category Two times Ones", 1, 5): {0: 40301, 2: 59699}, @@ -1413,68 +1386,68 @@ ("Category Two times Ones", 1, 7): {0: 28182, 2: 71818}, ("Category Two times Ones", 1, 8): {0: 23406, 2: 76594}, ("Category Two times Ones", 2, 0): {0: 100000}, - ("Category Two times Ones", 2, 1): {0: 69724, 2: 30276}, - ("Category Two times Ones", 2, 2): {0: 48238, 2: 42479, 4: 9283}, - ("Category Two times Ones", 2, 3): {0: 33290, 2: 48819, 4: 17891}, - ("Category Two times Ones", 2, 4): {0: 23136, 2: 49957, 4: 26907}, - ("Category Two times Ones", 2, 5): {0: 16146, 2: 48200, 4: 35654}, - ("Category Two times Ones", 2, 6): {0: 11083, 2: 44497, 4: 44420}, - ("Category Two times Ones", 2, 7): {0: 7662, 2: 40343, 4: 51995}, - ("Category Two times Ones", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Two times Ones", 2, 1): {0: 100000}, + ("Category Two times Ones", 2, 2): {0: 48238, 2: 51762}, + ("Category Two times Ones", 2, 3): {0: 33290, 4: 66710}, + ("Category Two times Ones", 2, 4): {0: 23136, 4: 76864}, + ("Category Two times Ones", 2, 5): {0: 16146, 4: 83854}, + ("Category Two times Ones", 2, 6): {0: 11083, 4: 88917}, + ("Category Two times Ones", 2, 7): {0: 7662, 4: 92338}, + ("Category Two times Ones", 2, 8): {0: 5354, 4: 94646}, ("Category Two times Ones", 3, 0): {0: 100000}, - ("Category Two times Ones", 3, 1): {0: 58021, 2: 34522, 4: 7457}, - ("Category Two times Ones", 3, 2): {0: 33548, 2: 44261, 4: 22191}, - ("Category Two times Ones", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, - ("Category Two times Ones", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, - ("Category Two times Ones", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, - ("Category Two times Ones", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, - ("Category Two times Ones", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, - ("Category Two times Ones", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Two times Ones", 3, 1): {0: 58021, 2: 41979}, + ("Category Two times Ones", 3, 2): {0: 33548, 4: 66452}, + ("Category Two times Ones", 3, 3): {0: 19375, 4: 80625}, + ("Category Two times Ones", 3, 4): {0: 10998, 4: 89002}, + ("Category Two times Ones", 3, 5): {0: 6519, 6: 93481}, + ("Category Two times Ones", 3, 6): {0: 3619, 6: 96381}, + ("Category Two times Ones", 3, 7): {0: 2195, 6: 97805}, + ("Category Two times Ones", 3, 8): {0: 13675, 6: 86325}, ("Category Two times Ones", 4, 0): {0: 100000}, - ("Category Two times Ones", 4, 1): {0: 48235, 2: 38602, 4: 13163}, - ("Category Two times Ones", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, - ("Category Two times Ones", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, - ("Category Two times Ones", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, - ("Category Two times Ones", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, - ("Category Two times Ones", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, - ("Category Two times Ones", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, - ("Category Two times Ones", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Two times Ones", 4, 1): {0: 48235, 2: 51765}, + ("Category Two times Ones", 4, 2): {0: 23289, 4: 76711}, + ("Category Two times Ones", 4, 3): {0: 11177, 6: 88823}, + ("Category Two times Ones", 4, 4): {0: 5499, 6: 94501}, + ("Category Two times Ones", 4, 5): {0: 18356, 6: 81644}, + ("Category Two times Ones", 4, 6): {0: 11169, 8: 88831}, + ("Category Two times Ones", 4, 7): {0: 6945, 8: 93055}, + ("Category Two times Ones", 4, 8): {0: 4091, 8: 95909}, ("Category Two times Ones", 5, 0): {0: 100000}, - ("Category Two times Ones", 5, 1): {0: 40028, 2: 40241, 4: 19731}, - ("Category Two times Ones", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, - ("Category Two times Ones", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, - ("Category Two times Ones", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, - ("Category Two times Ones", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, - ("Category Two times Ones", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, - ("Category Two times Ones", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, - ("Category Two times Ones", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Two times Ones", 5, 1): {0: 40028, 4: 59972}, + ("Category Two times Ones", 5, 2): {0: 16009, 6: 83991}, + ("Category Two times Ones", 5, 3): {0: 6489, 6: 93511}, + ("Category Two times Ones", 5, 4): {0: 16690, 8: 83310}, + ("Category Two times Ones", 5, 5): {0: 9016, 8: 90984}, + ("Category Two times Ones", 5, 6): {0: 4602, 8: 95398}, + ("Category Two times Ones", 5, 7): {0: 13627, 10: 86373}, + ("Category Two times Ones", 5, 8): {0: 8742, 10: 91258}, ("Category Two times Ones", 6, 0): {0: 100000}, - ("Category Two times Ones", 6, 1): {0: 33502, 2: 40413, 4: 26085}, - ("Category Two times Ones", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, - ("Category Two times Ones", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, - ("Category Two times Ones", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, - ("Category Two times Ones", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, - ("Category Two times Ones", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, - ("Category Two times Ones", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, - ("Category Two times Ones", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Two times Ones", 6, 1): {0: 33502, 4: 66498}, + ("Category Two times Ones", 6, 2): {0: 11210, 6: 88790}, + ("Category Two times Ones", 6, 3): {0: 3673, 6: 96327}, + ("Category Two times Ones", 6, 4): {0: 9291, 8: 90709}, + ("Category Two times Ones", 6, 5): {0: 441, 8: 99559}, + ("Category Two times Ones", 6, 6): {0: 10255, 10: 89745}, + ("Category Two times Ones", 6, 7): {0: 5646, 10: 94354}, + ("Category Two times Ones", 6, 8): {0: 14287, 12: 85713}, ("Category Two times Ones", 7, 0): {0: 100000}, - ("Category Two times Ones", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, - ("Category Two times Ones", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, - ("Category Two times Ones", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, - ("Category Two times Ones", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, - ("Category Two times Ones", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, - ("Category Two times Ones", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, - ("Category Two times Ones", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, - ("Category Two times Ones", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Two times Ones", 7, 1): {0: 27683, 4: 72317}, + ("Category Two times Ones", 7, 2): {0: 7824, 6: 92176}, + ("Category Two times Ones", 7, 3): {0: 13167, 8: 86833}, + ("Category Two times Ones", 7, 4): {0: 564, 10: 99436}, + ("Category Two times Ones", 7, 5): {0: 9824, 10: 90176}, + ("Category Two times Ones", 7, 6): {0: 702, 12: 99298}, + ("Category Two times Ones", 7, 7): {0: 10186, 12: 89814}, + ("Category Two times Ones", 7, 8): {0: 942, 12: 99058}, ("Category Two times Ones", 8, 0): {0: 100000}, - ("Category Two times Ones", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, - ("Category Two times Ones", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, - ("Category Two times Ones", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, - ("Category Two times Ones", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, - ("Category Two times Ones", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, - ("Category Two times Ones", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, - ("Category Two times Ones", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, - ("Category Two times Ones", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Two times Ones", 8, 1): {0: 23378, 4: 76622}, + ("Category Two times Ones", 8, 2): {0: 5420, 8: 94580}, + ("Category Two times Ones", 8, 3): {0: 8560, 10: 91440}, + ("Category Two times Ones", 8, 4): {0: 12199, 12: 87801}, + ("Category Two times Ones", 8, 5): {0: 879, 12: 99121}, + ("Category Two times Ones", 8, 6): {0: 9033, 14: 90967}, + ("Category Two times Ones", 8, 7): {0: 15767, 14: 84233}, + ("Category Two times Ones", 8, 8): {2: 9033, 14: 90967}, ("Category Half of Sixes", 0, 0): {0: 100000}, ("Category Half of Sixes", 0, 1): {0: 100000}, ("Category Half of Sixes", 0, 2): {0: 100000}, @@ -1485,7 +1458,7 @@ ("Category Half of Sixes", 0, 7): {0: 100000}, ("Category Half of Sixes", 0, 8): {0: 100000}, ("Category Half of Sixes", 1, 0): {0: 100000}, - ("Category Half of Sixes", 1, 1): {0: 83343, 3: 16657}, + ("Category Half of Sixes", 1, 1): {0: 100000}, ("Category Half of Sixes", 1, 2): {0: 69569, 3: 30431}, ("Category Half of Sixes", 1, 3): {0: 57872, 3: 42128}, ("Category Half of Sixes", 1, 4): {0: 48081, 3: 51919}, @@ -1495,1558 +1468,387 @@ ("Category Half of Sixes", 1, 8): {0: 23240, 3: 76760}, ("Category Half of Sixes", 2, 0): {0: 100000}, ("Category Half of Sixes", 2, 1): {0: 69419, 3: 30581}, - ("Category Half of Sixes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, - ("Category Half of Sixes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, - ("Category Half of Sixes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, - ("Category Half of Sixes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, - ("Category Half of Sixes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, - ("Category Half of Sixes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, - ("Category Half of Sixes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Half of Sixes", 2, 2): {0: 48202, 3: 51798}, + ("Category Half of Sixes", 2, 3): {0: 33376, 6: 66624}, + ("Category Half of Sixes", 2, 4): {0: 23276, 6: 76724}, + ("Category Half of Sixes", 2, 5): {0: 16092, 6: 83908}, + ("Category Half of Sixes", 2, 6): {0: 11232, 6: 88768}, + ("Category Half of Sixes", 2, 7): {0: 7589, 6: 92411}, + ("Category Half of Sixes", 2, 8): {0: 5447, 6: 94553}, ("Category Half of Sixes", 3, 0): {0: 100000}, - ("Category Half of Sixes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, - ("Category Half of Sixes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, - ("Category Half of Sixes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, - ("Category Half of Sixes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, - ("Category Half of Sixes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, - ("Category Half of Sixes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, - ("Category Half of Sixes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, - ("Category Half of Sixes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Half of Sixes", 3, 1): {0: 57964, 3: 42036}, + ("Category Half of Sixes", 3, 2): {0: 33637, 6: 66363}, + ("Category Half of Sixes", 3, 3): {0: 19520, 6: 80480}, + ("Category Half of Sixes", 3, 4): {0: 11265, 6: 88735}, + ("Category Half of Sixes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, + ("Category Half of Sixes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, + ("Category Half of Sixes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, + ("Category Half of Sixes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, ("Category Half of Sixes", 4, 0): {0: 100000}, - ("Category Half of Sixes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, - ("Category Half of Sixes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, - ("Category Half of Sixes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, - ("Category Half of Sixes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, - ("Category Half of Sixes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, - ("Category Half of Sixes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, - ("Category Half of Sixes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, - ("Category Half of Sixes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Half of Sixes", 4, 1): {0: 48121, 6: 51879}, + ("Category Half of Sixes", 4, 2): {0: 23296, 6: 76704}, + ("Category Half of Sixes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, + ("Category Half of Sixes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, + ("Category Half of Sixes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, + ("Category Half of Sixes", 4, 6): {0: 11267, 9: 88733}, + ("Category Half of Sixes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, + ("Category Half of Sixes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, ("Category Half of Sixes", 5, 0): {0: 100000}, - ("Category Half of Sixes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, - ("Category Half of Sixes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, - ("Category Half of Sixes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, - ("Category Half of Sixes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, - ("Category Half of Sixes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, - ("Category Half of Sixes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, - ("Category Half of Sixes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, - ("Category Half of Sixes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Half of Sixes", 5, 1): {0: 40183, 6: 59817}, + ("Category Half of Sixes", 5, 2): {0: 16197, 6: 83803}, + ("Category Half of Sixes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, + ("Category Half of Sixes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, + ("Category Half of Sixes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, + ("Category Half of Sixes", 5, 6): {0: 4652, 12: 95348}, + ("Category Half of Sixes", 5, 7): {0: 2365, 12: 97635}, + ("Category Half of Sixes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, ("Category Half of Sixes", 6, 0): {0: 100000}, - ("Category Half of Sixes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, - ("Category Half of Sixes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, - ("Category Half of Sixes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, - ("Category Half of Sixes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, - ("Category Half of Sixes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, - ("Category Half of Sixes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, - ("Category Half of Sixes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, - ("Category Half of Sixes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Half of Sixes", 6, 1): {0: 33473, 6: 66527}, + ("Category Half of Sixes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, + ("Category Half of Sixes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, + ("Category Half of Sixes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, + ("Category Half of Sixes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, + ("Category Half of Sixes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, + ("Category Half of Sixes", 6, 7): {0: 5519, 15: 94481}, + ("Category Half of Sixes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, ("Category Half of Sixes", 7, 0): {0: 100000}, - ("Category Half of Sixes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, - ("Category Half of Sixes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, - ("Category Half of Sixes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, - ("Category Half of Sixes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, - ("Category Half of Sixes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, - ("Category Half of Sixes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, - ("Category Half of Sixes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, - ("Category Half of Sixes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Half of Sixes", 7, 1): {0: 27933, 6: 72067}, + ("Category Half of Sixes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, + ("Category Half of Sixes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, + ("Category Half of Sixes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, + ("Category Half of Sixes", 7, 5): {0: 9894, 15: 90106}, + ("Category Half of Sixes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, + ("Category Half of Sixes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, + ("Category Half of Sixes", 7, 8): {0: 5710, 18: 94290}, ("Category Half of Sixes", 8, 0): {0: 100000}, - ("Category Half of Sixes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, - ("Category Half of Sixes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, - ("Category Half of Sixes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, - ("Category Half of Sixes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, - ("Category Half of Sixes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, - ("Category Half of Sixes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, - ("Category Half of Sixes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, - ("Category Half of Sixes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, - ("Category Twos and Threes", 1, 1): {0: 66466, 3: 33534}, - ("Category Twos and Threes", 1, 2): {0: 55640, 3: 44360}, - ("Category Twos and Threes", 1, 3): {0: 46223, 3: 53777}, - ("Category Twos and Threes", 1, 4): {0: 38552, 3: 61448}, - ("Category Twos and Threes", 1, 5): {0: 32320, 3: 67680}, - ("Category Twos and Threes", 1, 6): {0: 26733, 3: 73267}, - ("Category Twos and Threes", 1, 7): {0: 22289, 3: 77711}, - ("Category Twos and Threes", 1, 8): {0: 18676, 3: 81324}, - ("Category Twos and Threes", 2, 1): {0: 44565, 2: 21965, 3: 25172, 5: 8298}, - ("Category Twos and Threes", 2, 2): {0: 30855, 3: 51429, 6: 17716}, - ("Category Twos and Threes", 2, 3): {0: 21509, 3: 51178, 6: 27313}, - ("Category Twos and Threes", 2, 4): {0: 14935, 3: 48581, 6: 36484}, - ("Category Twos and Threes", 2, 5): {0: 10492, 3: 44256, 6: 45252}, - ("Category Twos and Threes", 2, 6): {0: 10775, 3: 35936, 6: 53289}, - ("Category Twos and Threes", 2, 7): {0: 7375, 3: 32469, 6: 60156}, - ("Category Twos and Threes", 2, 8): {0: 5212, 3: 35730, 6: 59058}, - ("Category Twos and Threes", 3, 1): {0: 29892, 2: 22136, 3: 27781, 6: 20191}, - ("Category Twos and Threes", 3, 2): {0: 17285, 3: 44257, 6: 38458}, - ("Category Twos and Threes", 3, 3): {0: 9889, 3: 36505, 6: 40112, 8: 13494}, - ("Category Twos and Threes", 3, 4): {0: 5717, 3: 28317, 6: 43044, 9: 22922}, - ("Category Twos and Threes", 3, 5): {0: 5795, 3: 19123, 6: 45004, 9: 30078}, - ("Category Twos and Threes", 3, 6): {0: 3273, 3: 21888, 6: 36387, 9: 38452}, - ("Category Twos and Threes", 3, 7): {0: 1917, 3: 16239, 6: 35604, 9: 46240}, - ("Category Twos and Threes", 3, 8): {0: 1124, 3: 12222, 6: 33537, 9: 53117}, - ("Category Twos and Threes", 4, 1): {0: 19619, 3: 46881, 6: 33500}, - ("Category Twos and Threes", 4, 2): {0: 9395, 3: 33926, 6: 37832, 9: 18847}, - ("Category Twos and Threes", 4, 3): {0: 4538, 3: 22968, 6: 38891, 9: 33603}, - ("Category Twos and Threes", 4, 4): {0: 4402, 3: 12654, 6: 35565, 9: 34784, 11: 12595}, - ("Category Twos and Threes", 4, 5): {0: 2065, 3: 14351, 6: 23592, 9: 38862, 12: 21130}, - ("Category Twos and Threes", 4, 6): {0: 1044, 3: 9056, 6: 20013, 9: 41255, 12: 28632}, - ("Category Twos and Threes", 4, 7): {0: 6310, 7: 24021, 9: 34297, 12: 35372}, - ("Category Twos and Threes", 4, 8): {0: 3694, 6: 18611, 9: 34441, 12: 43254}, - ("Category Twos and Threes", 5, 1): {0: 13070, 3: 33021, 5: 24568, 6: 16417, 8: 12924}, - ("Category Twos and Threes", 5, 2): {0: 5213, 3: 24275, 6: 37166, 9: 24746, 11: 8600}, - ("Category Twos and Threes", 5, 3): {0: 4707, 3: 10959, 6: 31388, 9: 33265, 12: 19681}, - ("Category Twos and Threes", 5, 4): {0: 1934, 3: 12081, 6: 17567, 9: 35282, 12: 33136}, - ("Category Twos and Threes", 5, 5): {0: 380, 2: 7025, 6: 13268, 9: 33274, 12: 33255, 14: 12798}, - ("Category Twos and Threes", 5, 6): {0: 3745, 6: 15675, 9: 22902, 12: 44665, 15: 13013}, - ("Category Twos and Threes", 5, 7): {0: 1969, 6: 10700, 9: 19759, 12: 39522, 15: 28050}, - ("Category Twos and Threes", 5, 8): {0: 13, 2: 7713, 10: 23957, 12: 32501, 15: 35816}, - ("Category Twos and Threes", 6, 1): {0: 8955, 3: 26347, 5: 24850, 8: 39848}, - ("Category Twos and Threes", 6, 2): {0: 2944, 3: 16894, 6: 32156, 9: 37468, 12: 10538}, - ("Category Twos and Threes", 6, 3): {0: 2484, 3: 13120, 6: 15999, 9: 32271, 12: 24898, 14: 11228}, - ("Category Twos and Threes", 6, 4): {0: 320, 2: 6913, 6: 10814, 9: 28622, 12: 31337, 15: 21994}, - ("Category Twos and Threes", 6, 5): {0: 3135, 6: 12202, 9: 16495, 12: 33605, 15: 26330, 17: 8233}, - ("Category Twos and Threes", 6, 6): {0: 98, 3: 8409, 9: 12670, 12: 31959, 15: 38296, 18: 8568}, - ("Category Twos and Threes", 6, 7): {0: 4645, 9: 15210, 12: 21906, 15: 44121, 18: 14118}, - ("Category Twos and Threes", 6, 8): {0: 2367, 9: 10679, 12: 18916, 15: 38806, 18: 29232}, - ("Category Twos and Threes", 7, 1): {0: 5802, 3: 28169, 6: 26411, 9: 31169, 11: 8449}, - ("Category Twos and Threes", 7, 2): {0: 4415, 6: 34992, 9: 31238, 12: 20373, 14: 8982}, - ("Category Twos and Threes", 7, 3): {0: 471, 2: 8571, 6: 10929, 9: 28058, 12: 28900, 14: 14953, 16: 8118}, - ("Category Twos and Threes", 7, 4): {0: 3487, 6: 12139, 9: 14001, 12: 30314, 15: 23096, 18: 16963}, - ("Category Twos and Threes", 7, 5): {0: 40, 2: 7460, 12: 36006, 15: 31388, 18: 25106}, - ("Category Twos and Threes", 7, 6): {0: 3554, 9: 11611, 12: 15116, 15: 32501, 18: 27524, 20: 9694}, - ("Category Twos and Threes", 7, 7): {0: 157, 6: 8396, 13: 19880, 15: 22333, 18: 39121, 21: 10113}, - ("Category Twos and Threes", 7, 8): {0: 31, 5: 4682, 12: 14446, 15: 20934, 18: 44127, 21: 15780}, - ("Category Twos and Threes", 8, 1): {0: 3799, 3: 22551, 6: 23754, 8: 29290, 10: 11990, 12: 8616}, - ("Category Twos and Threes", 8, 2): {0: 902, 4: 14360, 6: 13750, 9: 29893, 13: 30770, 15: 10325}, - ("Category Twos and Threes", 8, 3): {0: 2221, 4: 8122, 9: 23734, 12: 28527, 16: 28942, 18: 8454}, - ("Category Twos and Threes", 8, 4): {0: 140, 3: 8344, 12: 33635, 15: 28711, 18: 20093, 20: 9077}, - ("Category Twos and Threes", 8, 5): {0: 3601, 9: 10269, 12: 12458, 15: 28017, 18: 24815, 21: 20840}, - ("Category Twos and Threes", 8, 6): {0: 4104, 11: 10100, 15: 25259, 18: 30949, 21: 29588}, - ("Category Twos and Threes", 8, 7): {0: 3336, 12: 10227, 15: 14149, 18: 31155, 21: 29325, 23: 11808}, - ("Category Twos and Threes", 8, 8): {3: 7, 5: 7726, 16: 17997, 18: 21517, 21: 40641, 24: 12112}, - ("Category Sum of Odds", 1, 1): {0: 50084, 1: 16488, 3: 16584, 5: 16844}, - ("Category Sum of Odds", 1, 2): {0: 44489, 3: 27886, 5: 27625}, - ("Category Sum of Odds", 1, 3): {0: 27892, 3: 32299, 5: 39809}, - ("Category Sum of Odds", 1, 4): {0: 30917, 3: 19299, 5: 49784}, - ("Category Sum of Odds", 1, 5): {0: 25892, 3: 15941, 5: 58167}, - ("Category Sum of Odds", 1, 6): {0: 21678, 3: 13224, 5: 65098}, - ("Category Sum of Odds", 1, 7): {0: 17840, 3: 11191, 5: 70969}, - ("Category Sum of Odds", 1, 8): {0: 14690, 5: 85310}, - ("Category Sum of Odds", 2, 1): {0: 24611, 1: 19615, 3: 22234, 6: 25168, 8: 8372}, - ("Category Sum of Odds", 2, 2): {0: 11216, 3: 33181, 6: 32416, 8: 15414, 10: 7773}, - ("Category Sum of Odds", 2, 3): {0: 13730, 3: 17055, 5: 34933, 8: 18363, 10: 15919}, - ("Category Sum of Odds", 2, 4): {0: 9599, 3: 11842, 5: 34490, 8: 19129, 10: 24940}, - ("Category Sum of Odds", 2, 5): {0: 6652, 5: 40845, 8: 18712, 10: 33791}, - ("Category Sum of Odds", 2, 6): {0: 10404, 5: 20970, 8: 26124, 10: 42502}, - ("Category Sum of Odds", 2, 7): {0: 7262, 5: 26824, 8: 15860, 10: 50054}, - ("Category Sum of Odds", 2, 8): {0: 4950, 5: 23253, 8: 14179, 10: 57618}, - ("Category Sum of Odds", 3, 1): {0: 12467, 1: 16736, 4: 20970, 6: 29252, 8: 11660, 10: 8915}, - ("Category Sum of Odds", 3, 2): {0: 8635, 3: 15579, 6: 27649, 9: 30585, 13: 17552}, - ("Category Sum of Odds", 3, 3): {0: 5022, 6: 32067, 8: 21631, 11: 24032, 13: 17248}, - ("Category Sum of Odds", 3, 4): {0: 8260, 6: 17955, 8: 18530, 11: 28631, 13: 14216, 15: 12408}, - ("Category Sum of Odds", 3, 5): {0: 4685, 5: 13863, 8: 14915, 11: 30363, 13: 16370, 15: 19804}, - ("Category Sum of Odds", 3, 6): {0: 2766, 5: 10213, 8: 11372, 10: 30968, 13: 17133, 15: 27548}, - ("Category Sum of Odds", 3, 7): {0: 543, 3: 8448, 10: 28784, 13: 26258, 15: 35967}, - ("Category Sum of Odds", 3, 8): {0: 3760, 6: 8911, 11: 27672, 13: 16221, 15: 43436}, - ("Category Sum of Odds", 4, 1): {0: 18870, 5: 28873, 6: 18550, 9: 20881, 11: 12826}, - ("Category Sum of Odds", 4, 2): {0: 7974, 6: 23957, 9: 27982, 11: 15953, 13: 13643, 15: 10491}, - ("Category Sum of Odds", 4, 3): {0: 1778, 3: 8154, 8: 25036, 11: 24307, 13: 18030, 15: 14481, 18: 8214}, - ("Category Sum of Odds", 4, 4): {0: 1862, 4: 8889, 8: 11182, 11: 21997, 13: 19483, 16: 20879, 20: 15708}, - ("Category Sum of Odds", 4, 5): {0: 5687, 7: 8212, 11: 18674, 13: 17578, 16: 25572, 18: 12704, 20: 11573}, - ("Category Sum of Odds", 4, 6): {0: 6549, 11: 17161, 13: 15290, 16: 28355, 18: 14865, 20: 17780}, - ("Category Sum of Odds", 4, 7): {0: 5048, 10: 11824, 13: 12343, 16: 29544, 18: 15947, 20: 25294}, - ("Category Sum of Odds", 4, 8): {0: 3060, 10: 8747, 15: 29415, 18: 25762, 20: 33016}, - ("Category Sum of Odds", 5, 1): {0: 3061, 3: 22078, 6: 26935, 9: 23674, 11: 15144, 14: 9108}, - ("Category Sum of Odds", 5, 2): {0: 5813, 7: 19297, 9: 14666, 11: 17165, 14: 21681, 16: 10586, 18: 10792}, - ("Category Sum of Odds", 5, 3): {0: 3881, 6: 9272, 9: 10300, 11: 13443, 14: 24313, 16: 13969, 19: 16420, 21: 8402}, - ("Category Sum of Odds", 5, 4): {0: 4213, 8: 9656, 13: 24199, 16: 22188, 18: 16440, 20: 14313, 23: 8991}, - ("Category Sum of Odds", 5, 5): {0: 4997, 10: 9128, 13: 11376, 16: 20859, 18: 17548, 21: 20120, 25: 15972}, - ("Category Sum of Odds", 5, 6): { - 0: 4581, - 11: 8516, - 14: 11335, - 16: 10647, - 18: 16866, - 21: 24256, - 23: 11945, - 25: 11854, - }, - ("Category Sum of Odds", 5, 7): {0: 176, 6: 8052, 16: 17535, 18: 14878, 21: 27189, 23: 14100, 25: 18070}, - ("Category Sum of Odds", 5, 8): {0: 2, 2: 6622, 15: 12097, 18: 12454, 21: 28398, 23: 15254, 25: 25173}, - ("Category Sum of Odds", 6, 1): {0: 11634, 4: 12188, 6: 16257, 9: 23909, 11: 13671, 13: 13125, 16: 9216}, - ("Category Sum of Odds", 6, 2): {0: 1403, 4: 8241, 10: 22151, 12: 14245, 14: 15279, 17: 19690, 21: 18991}, - ("Category Sum of Odds", 6, 3): { - 0: 6079, - 9: 10832, - 12: 10094, - 14: 13221, - 17: 22538, - 19: 12673, - 21: 15363, - 24: 9200, - }, - ("Category Sum of Odds", 6, 4): {0: 5771, 11: 9419, 16: 22239, 19: 22715, 21: 12847, 23: 12798, 25: 9237, 28: 4974}, - ("Category Sum of Odds", 6, 5): { - 0: 2564, - 11: 8518, - 17: 20753, - 19: 14121, - 21: 13179, - 23: 15752, - 25: 14841, - 28: 10272, - }, - ("Category Sum of Odds", 6, 6): {0: 4310, 14: 8668, 19: 20891, 21: 12052, 23: 16882, 26: 19954, 30: 17243}, - ("Category Sum of Odds", 6, 7): { - 0: 5233, - 16: 8503, - 19: 11127, - 21: 10285, - 23: 16141, - 26: 23993, - 28: 12043, - 30: 12675, - }, - ("Category Sum of Odds", 6, 8): {0: 510, 12: 8107, 21: 17013, 23: 14396, 26: 26771, 28: 13964, 30: 19239}, - ("Category Sum of Odds", 7, 1): { - 0: 2591, - 2: 8436, - 5: 11759, - 7: 13733, - 9: 15656, - 11: 14851, - 13: 12301, - 15: 11871, - 18: 8802, - }, - ("Category Sum of Odds", 7, 2): { - 0: 4730, - 8: 8998, - 11: 10573, - 13: 13099, - 15: 13819, - 17: 13594, - 19: 12561, - 21: 12881, - 24: 9745, - }, - ("Category Sum of Odds", 7, 3): { - 0: 2549, - 9: 8523, - 15: 19566, - 17: 12251, - 19: 13562, - 21: 13473, - 23: 11918, - 27: 18158, - }, - ("Category Sum of Odds", 7, 4): {0: 6776, 14: 9986, 19: 20914, 22: 21006, 24: 12685, 26: 10835, 30: 17798}, - ("Category Sum of Odds", 7, 5): { - 0: 2943, - 14: 8009, - 20: 20248, - 22: 11896, - 24: 14166, - 26: 12505, - 28: 13136, - 30: 10486, - 33: 6611, - }, - ("Category Sum of Odds", 7, 6): { - 2: 1990, - 15: 8986, - 22: 19198, - 24: 13388, - 26: 12513, - 28: 15893, - 30: 15831, - 35: 12201, - }, - ("Category Sum of Odds", 7, 7): { - 4: 559, - 14: 8153, - 21: 11671, - 24: 12064, - 26: 11473, - 28: 16014, - 31: 20785, - 33: 10174, - 35: 9107, - }, - ("Category Sum of Odds", 7, 8): {0: 3, 8: 5190, 21: 8049, 24: 10585, 28: 25255, 31: 24333, 33: 12445, 35: 14140}, - ("Category Sum of Odds", 8, 1): {0: 7169, 7: 19762, 9: 14044, 11: 14858, 13: 13399, 15: 10801, 17: 11147, 20: 8820}, - ("Category Sum of Odds", 8, 2): { - 0: 7745, - 11: 10927, - 14: 10849, - 16: 13103, - 18: 13484, - 20: 12487, - 22: 10815, - 24: 11552, - 27: 9038, - }, - ("Category Sum of Odds", 8, 3): { - 0: 3867, - 12: 9356, - 18: 19408, - 20: 12379, - 22: 12519, - 24: 12260, - 26: 11008, - 28: 10726, - 31: 8477, - }, - ("Category Sum of Odds", 8, 4): { - 1: 3971, - 15: 9176, - 21: 18732, - 23: 12900, - 25: 13405, - 27: 11603, - 29: 10400, - 33: 19813, - }, - ("Category Sum of Odds", 8, 5): { - 1: 490, - 12: 8049, - 20: 9682, - 23: 10177, - 25: 12856, - 27: 12369, - 29: 12781, - 32: 18029, - 34: 11315, - 38: 4252, - }, - ("Category Sum of Odds", 8, 6): { - 4: 86, - 11: 8038, - 22: 9157, - 25: 10729, - 27: 11053, - 29: 13606, - 31: 12383, - 33: 14068, - 35: 12408, - 38: 8472, - }, - ("Category Sum of Odds", 8, 7): { - 6: 1852, - 20: 8020, - 27: 17455, - 29: 12898, - 31: 12181, - 33: 15650, - 35: 17577, - 40: 14367, - }, - ("Category Sum of Odds", 8, 8): { - 4: 8, - 11: 8008, - 26: 10314, - 29: 11446, - 31: 10714, - 33: 16060, - 36: 21765, - 38: 10622, - 40: 11063, - }, - ("Category Sum of Evens", 1, 1): {0: 49585, 2: 16733, 4: 16854, 6: 16828}, - ("Category Sum of Evens", 1, 2): {0: 33244, 2: 11087, 4: 28025, 6: 27644}, - ("Category Sum of Evens", 1, 3): {0: 22259, 4: 42357, 6: 35384}, - ("Category Sum of Evens", 1, 4): {0: 18511, 4: 35651, 6: 45838}, - ("Category Sum of Evens", 1, 5): {0: 15428, 4: 29656, 6: 54916}, - ("Category Sum of Evens", 1, 6): {0: 12927, 4: 24370, 6: 62703}, - ("Category Sum of Evens", 1, 7): {0: 14152, 4: 17087, 6: 68761}, - ("Category Sum of Evens", 1, 8): {0: 11920, 4: 14227, 6: 73853}, - ("Category Sum of Evens", 2, 1): {0: 25229, 2: 16545, 4: 19538, 6: 21987, 10: 16701}, - ("Category Sum of Evens", 2, 2): {0: 11179, 4: 27164, 6: 24451, 8: 13966, 10: 15400, 12: 7840}, - ("Category Sum of Evens", 2, 3): {0: 8099, 4: 16354, 6: 20647, 8: 17887, 10: 24736, 12: 12277}, - ("Category Sum of Evens", 2, 4): {0: 5687, 4: 11219, 6: 20711, 8: 14290, 10: 26976, 12: 21117}, - ("Category Sum of Evens", 2, 5): {0: 3991, 6: 27157, 8: 11641, 10: 26842, 12: 30369}, - ("Category Sum of Evens", 2, 6): {0: 2741, 6: 23123, 10: 35050, 12: 39086}, - ("Category Sum of Evens", 2, 7): {0: 1122, 6: 20538, 10: 30952, 12: 47388}, - ("Category Sum of Evens", 2, 8): {0: 3950, 6: 14006, 10: 27341, 12: 54703}, - ("Category Sum of Evens", 3, 1): {0: 12538, 2: 12516, 4: 16530, 6: 21270, 8: 13745, 10: 11209, 14: 12192}, - ("Category Sum of Evens", 3, 2): {0: 7404, 4: 10459, 6: 15644, 8: 15032, 10: 18955, 12: 15021, 16: 17485}, - ("Category Sum of Evens", 3, 3): {0: 2176, 6: 14148, 8: 12295, 10: 20247, 12: 18001, 14: 15953, 16: 17180}, - ("Category Sum of Evens", 3, 4): {0: 4556, 8: 15062, 10: 17232, 12: 18975, 14: 15832, 16: 18749, 18: 9594}, - ("Category Sum of Evens", 3, 5): {0: 2575, 8: 10825, 10: 13927, 12: 19533, 14: 14402, 16: 21954, 18: 16784}, - ("Category Sum of Evens", 3, 6): {0: 1475, 6: 7528, 10: 10614, 12: 19070, 14: 12940, 16: 23882, 18: 24491}, - ("Category Sum of Evens", 3, 7): {0: 862, 6: 5321, 12: 26291, 14: 10985, 16: 24254, 18: 32287}, - ("Category Sum of Evens", 3, 8): {0: 138, 4: 4086, 12: 22703, 16: 32516, 18: 40557}, - ("Category Sum of Evens", 4, 1): {0: 6214, 4: 20921, 6: 17434, 8: 15427, 10: 14158, 12: 11354, 16: 14492}, - ("Category Sum of Evens", 4, 2): { - 0: 2868, - 6: 13362, - 8: 10702, - 10: 15154, - 12: 15715, - 14: 14104, - 16: 12485, - 20: 15610, - }, - ("Category Sum of Evens", 4, 3): { - 0: 573, - 8: 10496, - 10: 10269, - 12: 12879, - 14: 16224, - 16: 17484, - 18: 13847, - 20: 10518, - 22: 7710, - }, - ("Category Sum of Evens", 4, 4): { - 0: 1119, - 6: 5124, - 12: 17394, - 14: 12763, - 16: 17947, - 18: 16566, - 20: 13338, - 22: 15749, - }, - ("Category Sum of Evens", 4, 5): {0: 3477, 12: 12738, 16: 26184, 18: 18045, 20: 14172, 22: 16111, 24: 9273}, - ("Category Sum of Evens", 4, 6): {0: 991, 12: 10136, 16: 21089, 18: 18805, 20: 13848, 22: 20013, 24: 15118}, - ("Category Sum of Evens", 4, 7): {0: 2931, 16: 21174, 18: 18952, 20: 12601, 22: 21947, 24: 22395}, - ("Category Sum of Evens", 4, 8): {0: 1798, 12: 6781, 18: 27146, 20: 11505, 22: 23056, 24: 29714}, - ("Category Sum of Evens", 5, 1): { - 0: 3192, - 4: 13829, - 6: 13373, - 8: 13964, - 10: 14656, - 12: 13468, - 14: 10245, - 18: 17273, - }, - ("Category Sum of Evens", 5, 2): { - 0: 3217, - 8: 10390, - 12: 22094, - 14: 13824, - 16: 14674, - 18: 12124, - 22: 16619, - 24: 7058, - }, - ("Category Sum of Evens", 5, 3): { - 0: 3904, - 12: 11004, - 14: 10339, - 16: 13128, - 18: 14686, - 20: 15282, - 22: 13294, - 26: 18363, - }, - ("Category Sum of Evens", 5, 4): { - 0: 43, - 4: 4025, - 14: 10648, - 16: 10437, - 18: 12724, - 20: 14710, - 22: 16005, - 24: 12896, - 28: 18512, - }, - ("Category Sum of Evens", 5, 5): { - 0: 350, - 8: 4392, - 16: 11641, - 18: 10297, - 20: 12344, - 22: 16826, - 24: 15490, - 26: 12235, - 28: 16425, - }, - ("Category Sum of Evens", 5, 6): { - 0: 374, - 10: 4670, - 18: 13498, - 22: 25729, - 24: 17286, - 26: 13565, - 28: 15274, - 30: 9604, - }, - ("Category Sum of Evens", 5, 7): {0: 1473, 18: 11310, 22: 21341, 24: 18114, 26: 13349, 28: 19048, 30: 15365}, - ("Category Sum of Evens", 5, 8): {0: 1, 4: 3753, 20: 10318, 22: 11699, 24: 18376, 26: 12500, 28: 21211, 30: 22142}, - ("Category Sum of Evens", 6, 1): { - 0: 4767, - 6: 15250, - 8: 11527, - 10: 13220, - 12: 13855, - 14: 12217, - 16: 10036, - 20: 19128, - }, - ("Category Sum of Evens", 6, 2): { - 0: 1380, - 6: 5285, - 12: 13888, - 14: 10495, - 16: 12112, - 18: 12962, - 20: 12458, - 22: 10842, - 26: 14076, - 28: 6502, - }, - ("Category Sum of Evens", 6, 3): { - 0: 1230, - 16: 17521, - 18: 10098, - 20: 12628, - 22: 13809, - 24: 13594, - 26: 11930, - 30: 19190, - }, - ("Category Sum of Evens", 6, 4): {0: 1235, 18: 15534, 22: 22081, 24: 13471, 26: 13991, 28: 12906, 32: 20782}, - ("Category Sum of Evens", 6, 5): {0: 1241, 20: 15114, 24: 21726, 26: 13874, 28: 15232, 30: 12927, 34: 19886}, - ("Category Sum of Evens", 6, 6): {0: 1224, 22: 15886, 26: 21708, 28: 15982, 30: 15534, 32: 12014, 34: 17652}, - ("Category Sum of Evens", 6, 7): {4: 1437, 24: 17624, 28: 24727, 30: 17083, 32: 13001, 34: 15604, 36: 10524}, - ("Category Sum of Evens", 6, 8): {4: 1707, 24: 11310, 28: 20871, 30: 18101, 32: 12842, 34: 18840, 36: 16329}, - ("Category Sum of Evens", 7, 1): { - 0: 6237, - 8: 15390, - 10: 11183, - 12: 12690, - 14: 12463, - 16: 11578, - 20: 17339, - 22: 8870, - 26: 4250, - }, - ("Category Sum of Evens", 7, 2): { - 0: 1433, - 14: 16705, - 18: 19797, - 20: 11747, - 22: 12101, - 24: 10947, - 28: 16547, - 32: 10723, - }, - ("Category Sum of Evens", 7, 3): { - 0: 2135, - 14: 5836, - 20: 13766, - 22: 10305, - 24: 12043, - 26: 13153, - 28: 12644, - 30: 10884, - 34: 19234, - }, - ("Category Sum of Evens", 7, 4): { - 0: 1762, - 22: 16471, - 26: 20839, - 28: 12907, - 30: 13018, - 32: 11907, - 34: 10022, - 38: 13074, - }, - ("Category Sum of Evens", 7, 5): { - 4: 1630, - 24: 14719, - 28: 20377, - 30: 12713, - 32: 13273, - 34: 13412, - 36: 10366, - 40: 13510, - }, - ("Category Sum of Evens", 7, 6): { - 4: 1436, - 26: 14275, - 30: 20680, - 32: 12798, - 34: 15385, - 36: 13346, - 38: 10011, - 40: 12069, - }, - ("Category Sum of Evens", 7, 7): { - 6: 2815, - 24: 6584, - 30: 16532, - 32: 11106, - 34: 15613, - 36: 15702, - 38: 12021, - 40: 12478, - 42: 7149, - }, - ("Category Sum of Evens", 7, 8): {10: 1490, 30: 16831, 34: 23888, 36: 16970, 38: 12599, 40: 16137, 42: 12085}, - ("Category Sum of Evens", 8, 1): { - 0: 3709, - 8: 10876, - 12: 19246, - 14: 11696, - 16: 11862, - 18: 11145, - 22: 16877, - 24: 9272, - 28: 5317, - }, - ("Category Sum of Evens", 8, 2): { - 0: 1361, - 16: 14530, - 20: 17637, - 22: 10922, - 24: 11148, - 26: 10879, - 30: 17754, - 34: 15769, - }, - ("Category Sum of Evens", 8, 3): { - 2: 1601, - 22: 14895, - 26: 18464, - 28: 11561, - 30: 12249, - 32: 11747, - 34: 10070, - 38: 19413, - }, - ("Category Sum of Evens", 8, 4): { - 0: 2339, - 20: 5286, - 26: 11746, - 30: 19858, - 32: 12344, - 34: 12243, - 36: 11307, - 40: 16632, - 42: 8245, - }, - ("Category Sum of Evens", 8, 5): { - 4: 1798, - 28: 14824, - 32: 18663, - 34: 12180, - 36: 12458, - 38: 12260, - 40: 10958, - 44: 16859, - }, - ("Category Sum of Evens", 8, 6): { - 6: 2908, - 26: 6292, - 32: 13573, - 34: 10367, - 36: 12064, - 38: 12862, - 40: 13920, - 42: 11359, - 46: 16655, - }, - ("Category Sum of Evens", 8, 7): { - 8: 2652, - 28: 6168, - 34: 13922, - 36: 10651, - 38: 12089, - 40: 14999, - 42: 13899, - 44: 10574, - 46: 15046, - }, - ("Category Sum of Evens", 8, 8): { - 10: 2547, - 30: 6023, - 36: 15354, - 38: 10354, - 40: 14996, - 42: 16214, - 44: 11803, - 46: 13670, - 48: 9039, - }, - ("Category Double Threes and Fours", 1, 1): {0: 66749, 6: 16591, 8: 16660}, - ("Category Double Threes and Fours", 1, 2): {0: 44675, 6: 27694, 8: 27631}, - ("Category Double Threes and Fours", 1, 3): {0: 29592, 6: 35261, 8: 35147}, - ("Category Double Threes and Fours", 1, 4): {0: 24601, 6: 29406, 8: 45993}, - ("Category Double Threes and Fours", 1, 5): {0: 20499, 6: 24420, 8: 55081}, - ("Category Double Threes and Fours", 1, 6): {0: 17116, 6: 20227, 8: 62657}, - ("Category Double Threes and Fours", 1, 7): {0: 14193, 6: 17060, 8: 68747}, - ("Category Double Threes and Fours", 1, 8): {0: 11977, 6: 13924, 8: 74099}, - ("Category Double Threes and Fours", 2, 1): {0: 44382, 6: 22191, 8: 22251, 14: 11176}, - ("Category Double Threes and Fours", 2, 2): {0: 19720, 6: 24652, 8: 24891, 14: 23096, 16: 7641}, - ("Category Double Threes and Fours", 2, 3): {0: 8765, 6: 21008, 8: 20929, 12: 12201, 14: 24721, 16: 12376}, - ("Category Double Threes and Fours", 2, 4): {0: 6164, 6: 14466, 8: 22828, 14: 35406, 16: 21136}, - ("Category Double Threes and Fours", 2, 5): {0: 4307, 6: 10005, 8: 22620, 14: 32879, 16: 30189}, - ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 28513, 14: 29530, 16: 39078}, - ("Category Double Threes and Fours", 2, 7): {0: 2042, 8: 24335, 14: 26250, 16: 47373}, - ("Category Double Threes and Fours", 2, 8): {0: 1385, 8: 23166, 14: 20907, 16: 54542}, - ("Category Double Threes and Fours", 3, 1): {0: 29378, 6: 22335, 8: 22138, 14: 16783, 16: 9366}, - ("Category Double Threes and Fours", 3, 2): { - 0: 8894, - 6: 16518, - 8: 16277, - 12: 10334, - 14: 20757, - 16: 12265, - 22: 14955, - }, - ("Category Double Threes and Fours", 3, 3): { - 0: 2643, - 8: 18522, - 12: 11066, - 14: 21922, - 16: 11045, - 20: 17235, - 22: 17567, - }, - ("Category Double Threes and Fours", 3, 4): { - 0: 1523, - 8: 13773, - 14: 26533, - 16: 18276, - 20: 11695, - 22: 18521, - 24: 9679, - }, - ("Category Double Threes and Fours", 3, 5): {0: 845, 8: 10218, 14: 20245, 16: 20293, 22: 31908, 24: 16491}, - ("Category Double Threes and Fours", 3, 6): {0: 499, 8: 7230, 14: 15028, 16: 20914, 22: 31835, 24: 24494}, - ("Category Double Threes and Fours", 3, 7): {0: 1298, 8: 5434, 16: 30595, 22: 29980, 24: 32693}, - ("Category Double Threes and Fours", 3, 8): {0: 178, 6: 4363, 16: 27419, 22: 27614, 24: 40426}, - ("Category Double Threes and Fours", 4, 1): {0: 19809, 6: 19538, 8: 19765, 14: 22348, 18: 12403, 22: 6137}, - ("Category Double Threes and Fours", 4, 2): { - 0: 3972, - 8: 19440, - 14: 27646, - 16: 12978, - 20: 11442, - 22: 11245, - 24: 6728, - 28: 6549, - }, - ("Category Double Threes and Fours", 4, 3): { - 0: 745, - 6: 7209, - 14: 19403, - 18: 11744, - 20: 15371, - 22: 15441, - 26: 13062, - 30: 17025, - }, - ("Category Double Threes and Fours", 4, 4): { - 0: 371, - 6: 4491, - 14: 13120, - 16: 10176, - 20: 11583, - 22: 18508, - 24: 10280, - 28: 15624, - 30: 15847, - }, - ("Category Double Threes and Fours", 4, 5): { - 0: 163, - 6: 4251, - 16: 15796, - 22: 26145, - 24: 17306, - 28: 10930, - 30: 16244, - 32: 9165, - }, - ("Category Double Threes and Fours", 4, 6): {0: 79, 16: 14439, 22: 21763, 24: 18861, 30: 29518, 32: 15340}, - ("Category Double Threes and Fours", 4, 7): {0: 1042, 16: 12543, 22: 13634, 24: 20162, 30: 30259, 32: 22360}, - ("Category Double Threes and Fours", 4, 8): {0: 20, 6: 2490, 16: 6901, 22: 10960, 24: 20269, 30: 29442, 32: 29918}, - ("Category Double Threes and Fours", 5, 1): { - 0: 13122, - 6: 16411, - 8: 16451, - 14: 24768, - 16: 10392, - 22: 14528, - 26: 4328, - }, - ("Category Double Threes and Fours", 5, 2): { - 0: 1676, - 8: 10787, - 14: 20218, - 18: 11102, - 20: 12668, - 22: 12832, - 26: 10994, - 30: 15390, - 34: 4333, - }, - ("Category Double Threes and Fours", 5, 3): { - 0: 223, - 14: 12365, - 16: 7165, - 20: 11385, - 22: 11613, - 26: 15182, - 28: 13665, - 32: 14400, - 36: 14002, - }, - ("Category Double Threes and Fours", 5, 4): { - 0: 95, - 6: 2712, - 16: 8862, - 22: 18696, - 26: 12373, - 28: 13488, - 30: 14319, - 34: 12414, - 38: 17041, - }, - ("Category Double Threes and Fours", 5, 5): { - 0: 1333, - 14: 5458, - 22: 13613, - 24: 10772, - 28: 11201, - 30: 16810, - 32: 10248, - 36: 14426, - 38: 16139, - }, - ("Category Double Threes and Fours", 5, 6): { - 0: 16, - 16: 6354, - 24: 16213, - 30: 25369, - 32: 16845, - 36: 10243, - 38: 15569, - 40: 9391, - }, - ("Category Double Threes and Fours", 5, 7): { - 0: 161, - 12: 3457, - 24: 12437, - 30: 21495, - 32: 18636, - 38: 28581, - 40: 15233, - }, - ("Category Double Threes and Fours", 5, 8): { - 0: 478, - 16: 4861, - 26: 10119, - 30: 13694, - 32: 19681, - 38: 29177, - 40: 21990, - }, - ("Category Double Threes and Fours", 6, 1): { - 0: 8738, - 6: 13463, - 8: 12988, - 14: 24653, - 16: 11068, - 22: 19621, - 26: 5157, - 30: 4312, - }, - ("Category Double Threes and Fours", 6, 2): { - 0: 784, - 6: 5735, - 14: 13407, - 16: 8170, - 20: 11349, - 22: 11356, - 26: 12465, - 28: 10790, - 30: 11527, - 38: 14417, - }, - ("Category Double Threes and Fours", 6, 3): { - 0: 72, - 14: 8986, - 22: 13700, - 26: 12357, - 28: 12114, - 32: 15882, - 36: 19286, - 40: 13540, - 44: 4063, - }, - ("Category Double Threes and Fours", 6, 4): { - 0: 439, - 18: 7427, - 22: 9284, - 28: 14203, - 30: 10836, - 34: 14646, - 36: 12511, - 38: 10194, - 42: 10202, - 46: 10258, - }, - ("Category Double Threes and Fours", 6, 5): { - 0: 166, - 20: 7618, - 24: 5198, - 30: 17479, - 34: 12496, - 36: 12190, - 38: 14163, - 42: 12571, - 46: 18119, - }, - ("Category Double Threes and Fours", 6, 6): { - 0: 1843, - 22: 5905, - 30: 12997, - 32: 10631, - 36: 10342, - 38: 16439, - 40: 10795, - 44: 13485, - 46: 17563, - }, - ("Category Double Threes and Fours", 6, 7): { - 0: 31, - 12: 2221, - 24: 5004, - 32: 15743, - 38: 24402, - 40: 17005, - 46: 25241, - 48: 10353, - }, - ("Category Double Threes and Fours", 6, 8): { - 8: 79, - 16: 4037, - 32: 12559, - 38: 20863, - 40: 18347, - 46: 27683, - 48: 16432, - }, - ("Category Double Threes and Fours", 7, 1): { - 0: 5803, - 6: 10242, - 8: 10404, - 14: 22886, - 16: 10934, - 22: 19133, - 24: 7193, - 28: 8167, - 32: 5238, - }, - ("Category Double Threes and Fours", 7, 2): { - 0: 357, - 14: 17082, - 22: 17524, - 26: 11974, - 28: 11132, - 32: 13186, - 36: 13959, - 40: 10028, - 44: 4758, - }, - ("Category Double Threes and Fours", 7, 3): { - 0: 361, - 18: 7136, - 22: 5983, - 28: 13899, - 32: 12974, - 34: 10088, - 36: 10081, - 40: 14481, - 44: 14127, - 46: 6547, - 50: 4323, - }, - ("Category Double Threes and Fours", 7, 4): { - 0: 1182, - 18: 4299, - 30: 16331, - 34: 11316, - 36: 10741, - 40: 16028, - 44: 18815, - 48: 15225, - 52: 6063, - }, - ("Category Double Threes and Fours", 7, 5): { - 0: 45, - 12: 3763, - 32: 17140, - 38: 19112, - 42: 13655, - 44: 11990, - 46: 11137, - 50: 10646, - 54: 12512, - }, - ("Category Double Threes and Fours", 7, 6): { - 8: 2400, - 28: 5277, - 32: 5084, - 38: 16047, - 42: 12133, - 44: 11451, - 46: 14027, - 50: 13198, - 54: 20383, - }, - ("Category Double Threes and Fours", 7, 7): { - 6: 1968, - 30: 5585, - 38: 12210, - 40: 10376, - 46: 25548, - 48: 15392, - 54: 21666, - 56: 7255, - }, - ("Category Double Threes and Fours", 7, 8): { - 8: 42, - 20: 2293, - 32: 4653, - 40: 15068, - 46: 23170, - 48: 17057, - 54: 25601, - 56: 12116, - }, - ("Category Double Threes and Fours", 8, 1): { - 0: 3982, - 8: 15658, - 14: 20388, - 16: 10234, - 20: 10167, - 22: 10162, - 28: 15330, - 32: 8758, - 36: 5321, - }, - ("Category Double Threes and Fours", 8, 2): { - 0: 161, - 6: 3169, - 14: 7106, - 22: 16559, - 28: 16400, - 32: 12950, - 36: 16399, - 40: 10090, - 44: 11474, - 48: 5692, - }, - ("Category Double Threes and Fours", 8, 3): { - 0: 856, - 16: 4092, - 30: 13686, - 34: 12838, - 38: 15010, - 42: 17085, - 46: 14067, - 50: 11844, - 52: 6500, - 56: 4022, - }, - ("Category Double Threes and Fours", 8, 4): { - 0: 36, - 12: 2795, - 30: 9742, - 36: 11726, - 40: 12404, - 44: 18791, - 48: 14662, - 52: 15518, - 54: 8066, - 58: 6260, - }, - ("Category Double Threes and Fours", 8, 5): { - 6: 8, - 12: 2948, - 30: 5791, - 38: 10658, - 42: 10175, - 46: 19359, - 50: 14449, - 52: 10531, - 56: 13257, - 60: 12824, - }, - ("Category Double Threes and Fours", 8, 6): { - 0: 2, - 12: 2528, - 32: 4832, - 40: 11436, - 46: 17832, - 50: 13016, - 52: 11631, - 54: 12058, - 58: 11458, - 62: 15207, - }, - ("Category Double Threes and Fours", 8, 7): { - 6: 2, - 12: 2204, - 40: 9320, - 46: 14688, - 50: 11494, - 52: 10602, - 54: 14541, - 58: 13849, - 62: 23300, - }, - ("Category Double Threes and Fours", 8, 8): { - 8: 1, - 16: 1773, - 42: 8766, - 48: 17452, - 54: 24338, - 56: 15722, - 62: 22745, - 64: 9203, - }, - ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 4: 16803, 8: 16630}, - ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 4: 27448, 8: 27743}, - ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 4: 23184, 8: 39716}, - ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 4: 19221, 8: 49816}, - ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 4: 16079, 8: 58605}, - ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 4: 13237, 8: 65258}, - ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 4: 11100, 8: 71224}, - ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 4: 9323, 8: 75706}, - ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 4: 22273, 8: 24842, 12: 8319}, - ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 4: 24890, 8: 32262, 12: 15172, 16: 7713}, - ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 4: 17158, 8: 34907, 12: 18539, 16: 15630}, - ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 4: 11981, 8: 34465, 12: 19108, 16: 24903}, - ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 4: 8302, 8: 32470, 12: 18612, 16: 34144}, - ("Category Quadruple Ones and Twos", 2, 6): {0: 4569, 4: 5737, 8: 29716, 12: 17216, 16: 42762}, - ("Category Quadruple Ones and Twos", 2, 7): {0: 3146, 8: 30463, 12: 15756, 16: 50635}, - ("Category Quadruple Ones and Twos", 2, 8): {0: 2265, 8: 26302, 12: 14167, 16: 57266}, - ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 4: 22574, 8: 27747, 12: 11557, 16: 8682}, - ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 4: 16295, 8: 26434, 12: 22986, 16: 16799, 20: 8629}, - ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 4: 9447, 8: 22255, 12: 21685, 16: 24084, 20: 11167, 24: 6299}, - ("Category Quadruple Ones and Twos", 3, 4): { - 0: 2864, - 4: 5531, - 8: 17681, - 12: 18400, - 16: 28524, - 20: 14552, - 24: 12448, - }, - ("Category Quadruple Ones and Twos", 3, 5): {0: 1676, 8: 16697, 12: 14755, 16: 30427, 20: 16602, 24: 19843}, - ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 8: 10259, 12: 11326, 16: 31125, 20: 16984, 24: 27625}, - ("Category Quadruple Ones and Twos", 3, 7): {0: 1688, 8: 7543, 12: 8769, 16: 29367, 20: 17085, 24: 35548}, - ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 8: 5277, 12: 6388, 16: 27741, 20: 16170, 24: 43483}, - ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 4: 19657, 8: 27288, 12: 16126, 16: 11167, 24: 6071}, - ("Category Quadruple Ones and Twos", 4, 2): { - 0: 4023, - 4: 9776, - 8: 19015, - 12: 22094, - 16: 20986, - 20: 13805, - 24: 10301, - }, - ("Category Quadruple Ones and Twos", 4, 3): { - 0: 1848, - 8: 17116, - 12: 16853, - 16: 22831, - 20: 18400, - 24: 14480, - 28: 8472, - }, - ("Category Quadruple Ones and Twos", 4, 4): { - 0: 930, - 8: 10375, - 12: 12063, - 16: 21220, - 20: 19266, - 24: 20615, - 28: 9443, - 32: 6088, - }, - ("Category Quadruple Ones and Twos", 4, 5): { - 0: 1561, - 12: 12612, - 16: 18209, - 20: 17910, - 24: 25474, - 28: 12864, - 32: 11370, - }, - ("Category Quadruple Ones and Twos", 4, 6): { - 0: 722, - 12: 7979, - 16: 14796, - 20: 15416, - 24: 28256, - 28: 14675, - 32: 18156, - }, - ("Category Quadruple Ones and Twos", 4, 7): { - 0: 115, - 12: 5304, - 16: 11547, - 20: 12289, - 24: 29181, - 28: 16052, - 32: 25512, - }, - ("Category Quadruple Ones and Twos", 4, 8): {0: 164, 8: 2971, 16: 8888, 20: 9679, 24: 28785, 28: 16180, 32: 33333}, - ("Category Quadruple Ones and Twos", 5, 1): { - 0: 13112, - 4: 16534, - 8: 24718, - 12: 18558, - 16: 14547, - 20: 7055, - 24: 5476, - }, - ("Category Quadruple Ones and Twos", 5, 2): { - 0: 1764, - 4: 5529, - 8: 12216, - 12: 17687, - 16: 20808, - 20: 18149, - 24: 12849, - 28: 6991, - 32: 4007, - }, - ("Category Quadruple Ones and Twos", 5, 3): { - 0: 719, - 8: 8523, - 12: 11074, - 16: 17322, - 20: 19002, - 24: 18643, - 28: 12827, - 32: 7960, - 36: 3930, - }, - ("Category Quadruple Ones and Twos", 5, 4): { - 0: 1152, - 12: 9790, - 16: 12913, - 20: 15867, - 24: 20749, - 28: 16398, - 32: 14218, - 36: 8913, - }, - ("Category Quadruple Ones and Twos", 5, 5): { - 0: 98, - 12: 5549, - 16: 8863, - 20: 12037, - 24: 20010, - 28: 17568, - 32: 19789, - 36: 9319, - 40: 6767, - }, - ("Category Quadruple Ones and Twos", 5, 6): { - 0: 194, - 8: 2663, - 16: 5734, - 20: 8436, - 24: 17830, - 28: 16864, - 32: 24246, - 36: 12115, - 40: 11918, - }, - ("Category Quadruple Ones and Twos", 5, 7): { - 0: 1449, - 20: 9396, - 24: 14936, - 28: 14969, - 32: 27238, - 36: 14094, - 40: 17918, - }, - ("Category Quadruple Ones and Twos", 5, 8): { - 0: 747, - 20: 6034, - 24: 11929, - 28: 12517, - 32: 28388, - 36: 15339, - 40: 25046, - }, - ("Category Quadruple Ones and Twos", 6, 1): { - 0: 8646, - 4: 13011, - 8: 21357, - 12: 19385, - 16: 17008, - 20: 10409, - 24: 6249, - 28: 3935, - }, - ("Category Quadruple Ones and Twos", 6, 2): { - 0: 844, - 8: 10311, - 12: 12792, - 16: 17480, - 20: 18814, - 24: 16492, - 28: 11889, - 32: 6893, - 36: 4485, - }, - ("Category Quadruple Ones and Twos", 6, 3): { - 0: 1241, - 12: 9634, - 16: 11685, - 20: 15584, - 24: 17967, - 28: 16506, - 32: 13314, - 36: 8034, - 40: 6035, - }, - ("Category Quadruple Ones and Twos", 6, 4): { - 0: 1745, - 16: 9804, - 20: 10562, - 24: 15746, - 28: 17174, - 32: 17787, - 36: 12820, - 40: 9289, - 44: 5073, - }, - ("Category Quadruple Ones and Twos", 6, 5): { - 0: 2076, - 20: 10247, - 24: 12264, - 28: 14810, - 32: 19588, - 36: 16002, - 40: 14682, - 44: 6410, - 48: 3921, - }, - ("Category Quadruple Ones and Twos", 6, 6): { - 0: 884, - 20: 5943, - 24: 8774, - 28: 11481, - 32: 19145, - 36: 16864, - 40: 19906, - 44: 9386, - 48: 7617, - }, - ("Category Quadruple Ones and Twos", 6, 7): { - 0: 1386, - 24: 8138, - 28: 8372, - 32: 17207, - 36: 16148, - 40: 24051, - 44: 11862, - 48: 12836, - }, - ("Category Quadruple Ones and Twos", 6, 8): { - 0: 1841, - 28: 9606, - 32: 14489, - 36: 14585, - 40: 26779, - 44: 13821, - 48: 18879, - }, - ("Category Quadruple Ones and Twos", 7, 1): { - 0: 5780, - 4: 10185, - 8: 17905, - 12: 18364, - 16: 18160, - 20: 13115, - 24: 8617, - 32: 7874, - }, - ("Category Quadruple Ones and Twos", 7, 2): { - 0: 1795, - 12: 12828, - 16: 13204, - 20: 16895, - 24: 17562, - 28: 15061, - 32: 11122, - 36: 6507, - 40: 5026, - }, - ("Category Quadruple Ones and Twos", 7, 3): { - 0: 2065, - 16: 10495, - 20: 11008, - 24: 14839, - 28: 16393, - 32: 16118, - 36: 12681, - 40: 8773, - 48: 7628, - }, - ("Category Quadruple Ones and Twos", 7, 4): { - 0: 1950, - 20: 9612, - 24: 10535, - 28: 13596, - 32: 16527, - 36: 15938, - 40: 14071, - 44: 9192, - 48: 8579, - }, - ("Category Quadruple Ones and Twos", 7, 5): { - 0: 223, - 20: 5144, - 24: 6337, - 28: 9400, - 32: 14443, - 36: 15955, - 40: 17820, - 44: 13369, - 48: 10702, - 56: 6607, - }, - ("Category Quadruple Ones and Twos", 7, 6): { - 0: 271, - 24: 5976, - 28: 5988, - 32: 11398, - 36: 13738, - 40: 19063, - 44: 15587, - 48: 15867, - 52: 7202, - 56: 4910, - }, - ("Category Quadruple Ones and Twos", 7, 7): { - 0: 1032, - 28: 5724, - 32: 8275, - 36: 10801, - 40: 18184, - 44: 16470, - 48: 20467, - 52: 9969, - 56: 9078, - }, - ("Category Quadruple Ones and Twos", 7, 8): { - 0: 1508, - 32: 7832, - 36: 7770, - 40: 16197, - 44: 15477, - 48: 24388, - 52: 12403, - 56: 14425, - }, - ("Category Quadruple Ones and Twos", 8, 1): { - 0: 3811, - 4: 7682, - 8: 14638, - 12: 17214, - 16: 18191, - 20: 14651, - 24: 10976, - 28: 6591, - 36: 6246, - }, - ("Category Quadruple Ones and Twos", 8, 2): { - 0: 906, - 12: 7768, - 16: 9421, - 20: 13623, - 24: 16213, - 28: 16246, - 32: 14131, - 36: 10076, - 40: 6198, - 48: 5418, - }, - ("Category Quadruple Ones and Twos", 8, 3): { - 0: 224, - 8: 2520, - 20: 11222, - 24: 10733, - 28: 13934, - 32: 15751, - 36: 14882, - 40: 12409, - 44: 8920, - 48: 5462, - 52: 3943, - }, - ("Category Quadruple Ones and Twos", 8, 4): { - 0: 233, - 20: 5163, - 24: 6057, - 28: 9073, - 32: 12990, - 36: 14756, - 40: 15851, - 44: 13795, - 48: 10706, - 52: 6310, - 56: 5066, - }, - ("Category Quadruple Ones and Twos", 8, 5): { - 0: 76, - 12: 2105, - 28: 8316, - 32: 8993, - 36: 12039, - 40: 15561, - 44: 15382, - 48: 15278, - 52: 10629, - 56: 7377, - 60: 4244, - }, - ("Category Quadruple Ones and Twos", 8, 6): { - 4: 262, - 32: 10321, - 36: 8463, - 40: 13177, - 44: 14818, - 48: 17731, - 52: 14024, - 56: 12425, - 60: 5446, - 64: 3333, - }, - ("Category Quadruple Ones and Twos", 8, 7): { - 8: 300, - 32: 5443, - 36: 5454, - 40: 10276, - 44: 12582, - 48: 18487, - 52: 15549, - 56: 17187, - 60: 8149, - 64: 6573, - }, - ("Category Quadruple Ones and Twos", 8, 8): { - 8: 354, - 36: 5678, - 40: 7484, - 44: 9727, - 48: 17080, - 52: 15898, - 56: 21877, - 60: 10773, - 64: 11129, - }, + ("Category Half of Sixes", 8, 1): {0: 23337, 6: 76663}, + ("Category Half of Sixes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, + ("Category Half of Sixes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, + ("Category Half of Sixes", 8, 4): {0: 291, 12: 59487, 18: 40222}, + ("Category Half of Sixes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, + ("Category Half of Sixes", 8, 6): {0: 8804, 18: 91196}, + ("Category Half of Sixes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, + ("Category Half of Sixes", 8, 8): {0: 9252, 21: 90748}, + ("Category Twos and Threes", 1, 1): {0: 66466, 2: 33534}, + ("Category Twos and Threes", 1, 2): {0: 55640, 2: 44360}, + ("Category Twos and Threes", 1, 3): {0: 57822, 3: 42178}, + ("Category Twos and Threes", 1, 4): {0: 48170, 3: 51830}, + ("Category Twos and Threes", 1, 5): {0: 40294, 3: 59706}, + ("Category Twos and Threes", 1, 6): {0: 33417, 3: 66583}, + ("Category Twos and Threes", 1, 7): {0: 27852, 3: 72148}, + ("Category Twos and Threes", 1, 8): {0: 23364, 3: 76636}, + ("Category Twos and Threes", 2, 1): {0: 44565, 3: 55435}, + ("Category Twos and Threes", 2, 2): {0: 46335, 3: 53665}, + ("Category Twos and Threes", 2, 3): {0: 32347, 3: 67653}, + ("Category Twos and Threes", 2, 4): {0: 22424, 5: 77576}, + ("Category Twos and Threes", 2, 5): {0: 15661, 6: 84339}, + ("Category Twos and Threes", 2, 6): {0: 10775, 6: 89225}, + ("Category Twos and Threes", 2, 7): {0: 7375, 6: 92625}, + ("Category Twos and Threes", 2, 8): {0: 5212, 6: 94788}, + ("Category Twos and Threes", 3, 1): {0: 29892, 3: 70108}, + ("Category Twos and Threes", 3, 2): {0: 17285, 5: 82715}, + ("Category Twos and Threes", 3, 3): {0: 17436, 6: 82564}, + ("Category Twos and Threes", 3, 4): {0: 9962, 6: 90038}, + ("Category Twos and Threes", 3, 5): {0: 3347, 6: 96653}, + ("Category Twos and Threes", 3, 6): {0: 1821, 8: 98179}, + ("Category Twos and Threes", 3, 7): {0: 1082, 6: 61417, 9: 37501}, + ("Category Twos and Threes", 3, 8): {0: 13346, 9: 86654}, + ("Category Twos and Threes", 4, 1): {0: 19619, 5: 80381}, + ("Category Twos and Threes", 4, 2): {0: 18914, 6: 81086}, + ("Category Twos and Threes", 4, 3): {0: 4538, 5: 61859, 8: 33603}, + ("Category Twos and Threes", 4, 4): {0: 2183, 6: 62279, 9: 35538}, + ("Category Twos and Threes", 4, 5): {0: 16416, 9: 83584}, + ("Category Twos and Threes", 4, 6): {0: 6285, 9: 93715}, + ("Category Twos and Threes", 4, 7): {0: 30331, 11: 69669}, + ("Category Twos and Threes", 4, 8): {0: 22305, 12: 77695}, + ("Category Twos and Threes", 5, 1): {0: 13070, 5: 86930}, + ("Category Twos and Threes", 5, 2): {0: 5213, 5: 61441, 8: 33346}, + ("Category Twos and Threes", 5, 3): {0: 2126, 6: 58142, 9: 39732}, + ("Category Twos and Threes", 5, 4): {0: 848, 2: 30734, 11: 68418}, + ("Category Twos and Threes", 5, 5): {0: 29502, 12: 70498}, + ("Category Twos and Threes", 5, 6): {0: 123, 9: 52792, 12: 47085}, + ("Category Twos and Threes", 5, 7): {0: 8241, 12: 91759}, + ("Category Twos and Threes", 5, 8): {0: 13, 2: 31670, 14: 68317}, + ("Category Twos and Threes", 6, 1): {0: 22090, 6: 77910}, + ("Category Twos and Threes", 6, 2): {0: 2944, 6: 62394, 9: 34662}, + ("Category Twos and Threes", 6, 3): {0: 977, 2: 30626, 11: 68397}, + ("Category Twos and Threes", 6, 4): {0: 320, 8: 58370, 12: 41310}, + ("Category Twos and Threes", 6, 5): {0: 114, 2: 31718, 14: 68168}, + ("Category Twos and Threes", 6, 6): {0: 29669, 15: 70331}, + ("Category Twos and Threes", 6, 7): {0: 19855, 15: 80145}, + ("Category Twos and Threes", 6, 8): {0: 8524, 15: 91476}, + ("Category Twos and Threes", 7, 1): {0: 5802, 4: 54580, 7: 39618}, + ("Category Twos and Threes", 7, 2): {0: 1605, 6: 62574, 10: 35821}, + ("Category Twos and Threes", 7, 3): {0: 471, 8: 59691, 12: 39838}, + ("Category Twos and Threes", 7, 4): {0: 26620, 14: 73380}, + ("Category Twos and Threes", 7, 5): {0: 17308, 11: 37515, 15: 45177}, + ("Category Twos and Threes", 7, 6): {0: 30281, 17: 69719}, + ("Category Twos and Threes", 7, 7): {0: 28433, 18: 71567}, + ("Category Twos and Threes", 7, 8): {0: 13274, 18: 86726}, + ("Category Twos and Threes", 8, 1): {0: 3799, 5: 56614, 8: 39587}, + ("Category Twos and Threes", 8, 2): {0: 902, 7: 58003, 11: 41095}, + ("Category Twos and Threes", 8, 3): {0: 29391, 14: 70609}, + ("Category Twos and Threes", 8, 4): {0: 26041, 12: 40535, 16: 33424}, + ("Category Twos and Threes", 8, 5): {0: 26328, 14: 38760, 18: 34912}, + ("Category Twos and Threes", 8, 6): {0: 22646, 15: 45218, 19: 32136}, + ("Category Twos and Threes", 8, 7): {0: 25908, 20: 74092}, + ("Category Twos and Threes", 8, 8): {3: 18441, 17: 38826, 21: 42733}, + ("Category Sum of Odds", 1, 1): {0: 66572, 5: 33428}, + ("Category Sum of Odds", 1, 2): {0: 44489, 5: 55511}, + ("Category Sum of Odds", 1, 3): {0: 37185, 5: 62815}, + ("Category Sum of Odds", 1, 4): {0: 30917, 5: 69083}, + ("Category Sum of Odds", 1, 5): {0: 41833, 5: 58167}, + ("Category Sum of Odds", 1, 6): {0: 34902, 5: 65098}, + ("Category Sum of Odds", 1, 7): {0: 29031, 5: 70969}, + ("Category Sum of Odds", 1, 8): {0: 24051, 5: 75949}, + ("Category Sum of Odds", 2, 1): {0: 66460, 5: 33540}, + ("Category Sum of Odds", 2, 2): {0: 11216, 5: 65597, 8: 23187}, + ("Category Sum of Odds", 2, 3): {0: 30785, 8: 69215}, + ("Category Sum of Odds", 2, 4): {0: 21441, 10: 78559}, + ("Category Sum of Odds", 2, 5): {0: 14948, 10: 85052}, + ("Category Sum of Odds", 2, 6): {0: 4657, 3: 35569, 10: 59774}, + ("Category Sum of Odds", 2, 7): {0: 7262, 5: 42684, 10: 50054}, + ("Category Sum of Odds", 2, 8): {0: 4950, 5: 37432, 10: 57618}, + ("Category Sum of Odds", 3, 1): {0: 29203, 6: 70797}, + ("Category Sum of Odds", 3, 2): {0: 34454, 9: 65546}, + ("Category Sum of Odds", 3, 3): {0: 5022, 3: 32067, 8: 45663, 13: 17248}, + ("Category Sum of Odds", 3, 4): {0: 6138, 4: 33396, 13: 60466}, + ("Category Sum of Odds", 3, 5): {0: 29405, 15: 70595}, + ("Category Sum of Odds", 3, 6): {0: 21390, 15: 78610}, + ("Category Sum of Odds", 3, 7): {0: 8991, 8: 38279, 15: 52730}, + ("Category Sum of Odds", 3, 8): {0: 6340, 8: 34003, 15: 59657}, + ("Category Sum of Odds", 4, 1): {0: 28095, 4: 38198, 8: 33707}, + ("Category Sum of Odds", 4, 2): {0: 27003, 11: 72997}, + ("Category Sum of Odds", 4, 3): {0: 18712, 8: 40563, 13: 40725}, + ("Category Sum of Odds", 4, 4): {0: 30691, 15: 69309}, + ("Category Sum of Odds", 4, 5): {0: 433, 3: 32140, 13: 43150, 18: 24277}, + ("Category Sum of Odds", 4, 6): {0: 6549, 9: 32451, 15: 43220, 20: 17780}, + ("Category Sum of Odds", 4, 7): {0: 29215, 15: 45491, 20: 25294}, + ("Category Sum of Odds", 4, 8): {0: 11807, 13: 38927, 20: 49266}, + ("Category Sum of Odds", 5, 1): {0: 25139, 9: 74861}, + ("Category Sum of Odds", 5, 2): {0: 25110, 9: 40175, 14: 34715}, + ("Category Sum of Odds", 5, 3): {0: 23453, 11: 37756, 16: 38791}, + ("Category Sum of Odds", 5, 4): {0: 22993, 13: 37263, 18: 39744}, + ("Category Sum of Odds", 5, 5): {0: 25501, 15: 38407, 20: 36092}, + ("Category Sum of Odds", 5, 6): {0: 2542, 10: 32537, 18: 41122, 23: 23799}, + ("Category Sum of Odds", 5, 7): {0: 8228, 14: 32413, 20: 41289, 25: 18070}, + ("Category Sum of Odds", 5, 8): {0: 2, 2: 31173, 20: 43652, 25: 25173}, + ("Category Sum of Odds", 6, 1): {0: 23822, 6: 40166, 11: 36012}, + ("Category Sum of Odds", 6, 2): {0: 24182, 11: 37137, 16: 38681}, + ("Category Sum of Odds", 6, 3): {0: 27005, 14: 35759, 19: 37236}, + ("Category Sum of Odds", 6, 4): {0: 25133, 16: 35011, 21: 39856}, + ("Category Sum of Odds", 6, 5): {0: 24201, 18: 34934, 23: 40865}, + ("Category Sum of Odds", 6, 6): {0: 12978, 17: 32943, 23: 36836, 28: 17243}, + ("Category Sum of Odds", 6, 7): {0: 2314, 14: 32834, 23: 40134, 28: 24718}, + ("Category Sum of Odds", 6, 8): {0: 5464, 18: 34562, 25: 40735, 30: 19239}, + ("Category Sum of Odds", 7, 1): {0: 29329, 8: 37697, 13: 32974}, + ("Category Sum of Odds", 7, 2): {0: 29935, 14: 34878, 19: 35187}, + ("Category Sum of Odds", 7, 3): {0: 30638, 17: 33733, 22: 35629}, + ("Category Sum of Odds", 7, 4): {0: 163, 6: 32024, 20: 33870, 25: 33943}, + ("Category Sum of Odds", 7, 5): {0: 31200, 22: 35565, 27: 33235}, + ("Category Sum of Odds", 7, 6): {2: 30174, 24: 36670, 29: 33156}, + ("Category Sum of Odds", 7, 7): {4: 8712, 21: 35208, 28: 36799, 33: 19281}, + ("Category Sum of Odds", 7, 8): {0: 1447, 18: 32027, 28: 39941, 33: 26585}, + ("Category Sum of Odds", 8, 1): {0: 26931, 9: 35423, 14: 37646}, + ("Category Sum of Odds", 8, 2): {0: 29521, 16: 32919, 21: 37560}, + ("Category Sum of Odds", 8, 3): {0: 412, 7: 32219, 20: 32055, 25: 35314}, + ("Category Sum of Odds", 8, 4): {1: 27021, 22: 36376, 28: 36603}, + ("Category Sum of Odds", 8, 5): {1: 1069, 14: 32451, 26: 32884, 31: 33596}, + ("Category Sum of Odds", 8, 6): {4: 31598, 28: 33454, 33: 34948}, + ("Category Sum of Odds", 8, 7): {6: 27327, 29: 35647, 34: 37026}, + ("Category Sum of Odds", 8, 8): {4: 1, 26: 40489, 33: 37825, 38: 21685}, + ("Category Sum of Evens", 1, 1): {0: 49585, 6: 50415}, + ("Category Sum of Evens", 1, 2): {0: 44331, 6: 55669}, + ("Category Sum of Evens", 1, 3): {0: 29576, 6: 70424}, + ("Category Sum of Evens", 1, 4): {0: 24744, 6: 75256}, + ("Category Sum of Evens", 1, 5): {0: 20574, 6: 79426}, + ("Category Sum of Evens", 1, 6): {0: 17182, 6: 82818}, + ("Category Sum of Evens", 1, 7): {0: 14152, 6: 85848}, + ("Category Sum of Evens", 1, 8): {0: 8911, 6: 91089}, + ("Category Sum of Evens", 2, 1): {0: 25229, 8: 74771}, + ("Category Sum of Evens", 2, 2): {0: 18682, 6: 58078, 10: 23240}, + ("Category Sum of Evens", 2, 3): {0: 8099, 10: 91901}, + ("Category Sum of Evens", 2, 4): {0: 16906, 12: 83094}, + ("Category Sum of Evens", 2, 5): {0: 11901, 12: 88099}, + ("Category Sum of Evens", 2, 6): {0: 8054, 12: 91946}, + ("Category Sum of Evens", 2, 7): {0: 5695, 12: 94305}, + ("Category Sum of Evens", 2, 8): {0: 3950, 12: 96050}, + ("Category Sum of Evens", 3, 1): {0: 25054, 6: 51545, 10: 23401}, + ("Category Sum of Evens", 3, 2): {0: 17863, 10: 64652, 14: 17485}, + ("Category Sum of Evens", 3, 3): {0: 7748, 12: 75072, 16: 17180}, + ("Category Sum of Evens", 3, 4): {0: 1318, 12: 70339, 16: 28343}, + ("Category Sum of Evens", 3, 5): {0: 7680, 12: 53582, 18: 38738}, + ("Category Sum of Evens", 3, 6): {0: 1475, 12: 50152, 18: 48373}, + ("Category Sum of Evens", 3, 7): {0: 14328, 18: 85672}, + ("Category Sum of Evens", 3, 8): {0: 10001, 18: 89999}, + ("Category Sum of Evens", 4, 1): {0: 6214, 8: 67940, 12: 25846}, + ("Category Sum of Evens", 4, 2): {0: 16230, 12: 55675, 16: 28095}, + ("Category Sum of Evens", 4, 3): {0: 11069, 16: 70703, 20: 18228}, + ("Category Sum of Evens", 4, 4): {0: 13339, 20: 86661}, + ("Category Sum of Evens", 4, 5): {0: 8193, 18: 66423, 22: 25384}, + ("Category Sum of Evens", 4, 6): {0: 11127, 18: 53742, 22: 35131}, + ("Category Sum of Evens", 4, 7): {0: 7585, 18: 48073, 24: 44342}, + ("Category Sum of Evens", 4, 8): {0: 642, 18: 46588, 24: 52770}, + ("Category Sum of Evens", 5, 1): {0: 8373, 8: 50641, 16: 40986}, + ("Category Sum of Evens", 5, 2): {0: 7271, 12: 42254, 20: 50475}, + ("Category Sum of Evens", 5, 3): {0: 8350, 16: 44711, 24: 46939}, + ("Category Sum of Evens", 5, 4): {0: 8161, 18: 44426, 26: 47413}, + ("Category Sum of Evens", 5, 5): {0: 350, 8: 16033, 24: 67192, 28: 16425}, + ("Category Sum of Evens", 5, 6): {0: 10318, 24: 64804, 28: 24878}, + ("Category Sum of Evens", 5, 7): {0: 12783, 24: 52804, 28: 34413}, + ("Category Sum of Evens", 5, 8): {0: 1, 24: 56646, 30: 43353}, + ("Category Sum of Evens", 6, 1): {0: 10482, 10: 48137, 18: 41381}, + ("Category Sum of Evens", 6, 2): {0: 12446, 16: 43676, 24: 43878}, + ("Category Sum of Evens", 6, 3): {0: 11037, 20: 44249, 28: 44714}, + ("Category Sum of Evens", 6, 4): {0: 10005, 22: 42316, 30: 47679}, + ("Category Sum of Evens", 6, 5): {0: 9751, 24: 42204, 32: 48045}, + ("Category Sum of Evens", 6, 6): {0: 9692, 26: 45108, 34: 45200}, + ("Category Sum of Evens", 6, 7): {4: 1437, 26: 42351, 34: 56212}, + ("Category Sum of Evens", 6, 8): {4: 13017, 30: 51814, 36: 35169}, + ("Category Sum of Evens", 7, 1): {0: 12688, 12: 45275, 20: 42037}, + ("Category Sum of Evens", 7, 2): {0: 1433, 20: 60350, 28: 38217}, + ("Category Sum of Evens", 7, 3): {0: 13724, 24: 43514, 32: 42762}, + ("Category Sum of Evens", 7, 4): {0: 11285, 26: 40694, 34: 48021}, + ("Category Sum of Evens", 7, 5): {4: 5699, 28: 43740, 36: 50561}, + ("Category Sum of Evens", 7, 6): {4: 5478, 30: 43711, 38: 50811}, + ("Category Sum of Evens", 7, 7): {6: 9399, 32: 43251, 40: 47350}, + ("Category Sum of Evens", 7, 8): {10: 1490, 32: 40719, 40: 57791}, + ("Category Sum of Evens", 8, 1): {0: 14585, 14: 42804, 22: 42611}, + ("Category Sum of Evens", 8, 2): {0: 15891, 22: 39707, 30: 44402}, + ("Category Sum of Evens", 8, 3): {2: 297, 12: 16199, 28: 42274, 36: 41230}, + ("Category Sum of Evens", 8, 4): {0: 7625, 30: 43948, 38: 48427}, + ("Category Sum of Evens", 8, 5): {4: 413, 18: 16209, 34: 43301, 42: 40077}, + ("Category Sum of Evens", 8, 6): {6: 14927, 36: 43139, 44: 41934}, + ("Category Sum of Evens", 8, 7): {8: 5042, 36: 40440, 44: 54518}, + ("Category Sum of Evens", 8, 8): {10: 5005, 38: 44269, 46: 50726}, + ("Category Double Threes and Fours", 1, 1): {0: 66749, 8: 33251}, + ("Category Double Threes and Fours", 1, 2): {0: 44675, 8: 55325}, + ("Category Double Threes and Fours", 1, 3): {0: 29592, 8: 70408}, + ("Category Double Threes and Fours", 1, 4): {0: 24601, 8: 75399}, + ("Category Double Threes and Fours", 1, 5): {0: 20499, 8: 79501}, + ("Category Double Threes and Fours", 1, 6): {0: 17116, 8: 82884}, + ("Category Double Threes and Fours", 1, 7): {0: 14193, 8: 85807}, + ("Category Double Threes and Fours", 1, 8): {0: 11977, 8: 88023}, + ("Category Double Threes and Fours", 2, 1): {0: 44382, 8: 55618}, + ("Category Double Threes and Fours", 2, 2): {0: 19720, 8: 57236, 14: 23044}, + ("Category Double Threes and Fours", 2, 3): {0: 8765, 8: 41937, 14: 49298}, + ("Category Double Threes and Fours", 2, 4): {0: 6164, 16: 93836}, + ("Category Double Threes and Fours", 2, 5): {0: 4307, 8: 38682, 16: 57011}, + ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 32717, 16: 64404}, + ("Category Double Threes and Fours", 2, 7): {0: 6679, 16: 93321}, + ("Category Double Threes and Fours", 2, 8): {0: 4758, 16: 95242}, + ("Category Double Threes and Fours", 3, 1): {0: 29378, 8: 50024, 14: 20598}, + ("Category Double Threes and Fours", 3, 2): {0: 8894, 14: 74049, 18: 17057}, + ("Category Double Threes and Fours", 3, 3): {0: 2643, 14: 62555, 22: 34802}, + ("Category Double Threes and Fours", 3, 4): {0: 1523, 6: 19996, 16: 50281, 22: 28200}, + ("Category Double Threes and Fours", 3, 5): {0: 845, 16: 60496, 24: 38659}, + ("Category Double Threes and Fours", 3, 6): {0: 499, 16: 51131, 24: 48370}, + ("Category Double Threes and Fours", 3, 7): {0: 5542, 16: 37755, 24: 56703}, + ("Category Double Threes and Fours", 3, 8): {0: 3805, 16: 32611, 24: 63584}, + ("Category Double Threes and Fours", 4, 1): {0: 19809, 8: 39303, 16: 40888}, + ("Category Double Threes and Fours", 4, 2): {0: 3972, 16: 71506, 22: 24522}, + ("Category Double Threes and Fours", 4, 3): {0: 745, 18: 53727, 22: 28503, 28: 17025}, + ("Category Double Threes and Fours", 4, 4): {0: 4862, 16: 34879, 22: 33529, 28: 26730}, + ("Category Double Threes and Fours", 4, 5): {0: 2891, 16: 25367, 24: 46333, 30: 25409}, + ("Category Double Threes and Fours", 4, 6): {0: 2525, 24: 62353, 30: 35122}, + ("Category Double Threes and Fours", 4, 7): {0: 1042, 24: 54543, 32: 44415}, + ("Category Double Threes and Fours", 4, 8): {0: 2510, 24: 44681, 32: 52809}, + ("Category Double Threes and Fours", 5, 1): {0: 13122, 14: 68022, 20: 18856}, + ("Category Double Threes and Fours", 5, 2): {0: 1676, 14: 37791, 22: 40810, 28: 19723}, + ("Category Double Threes and Fours", 5, 3): {0: 2945, 16: 28193, 22: 26795, 32: 42067}, + ("Category Double Threes and Fours", 5, 4): {0: 2807, 26: 53419, 30: 26733, 36: 17041}, + ("Category Double Threes and Fours", 5, 5): {0: 3651, 24: 38726, 32: 41484, 38: 16139}, + ("Category Double Threes and Fours", 5, 6): {0: 362, 12: 13070, 32: 61608, 38: 24960}, + ("Category Double Threes and Fours", 5, 7): {0: 161, 12: 15894, 32: 49464, 38: 34481}, + ("Category Double Threes and Fours", 5, 8): {0: 82, 12: 11438, 32: 45426, 40: 43054}, + ("Category Double Threes and Fours", 6, 1): {0: 8738, 6: 26451, 16: 43879, 22: 20932}, + ("Category Double Threes and Fours", 6, 2): {0: 784, 16: 38661, 28: 42164, 32: 18391}, + ("Category Double Threes and Fours", 6, 3): {0: 1062, 22: 34053, 28: 27996, 38: 36889}, + ("Category Double Threes and Fours", 6, 4): {0: 439, 12: 13100, 30: 43296, 40: 43165}, + ("Category Double Threes and Fours", 6, 5): {0: 3957, 34: 51190, 38: 26734, 44: 18119}, + ("Category Double Threes and Fours", 6, 6): {0: 4226, 32: 37492, 40: 40719, 46: 17563}, + ("Category Double Threes and Fours", 6, 7): {0: 31, 12: 13933, 40: 60102, 46: 25934}, + ("Category Double Threes and Fours", 6, 8): {8: 388, 22: 16287, 40: 48255, 48: 35070}, + ("Category Double Threes and Fours", 7, 1): {0: 5803, 8: 28280, 14: 26186, 26: 39731}, + ("Category Double Threes and Fours", 7, 2): {0: 3319, 20: 36331, 30: 38564, 36: 21786}, + ("Category Double Threes and Fours", 7, 3): {0: 2666, 18: 16444, 34: 41412, 44: 39478}, + ("Category Double Threes and Fours", 7, 4): {0: 99, 12: 9496, 38: 50302, 46: 40103}, + ("Category Double Threes and Fours", 7, 5): {0: 45, 12: 13200, 42: 52460, 50: 34295}, + ("Category Double Threes and Fours", 7, 6): {8: 2400, 28: 16653, 46: 60564, 52: 20383}, + ("Category Double Threes and Fours", 7, 7): {6: 7, 12: 11561, 44: 44119, 54: 44313}, + ("Category Double Threes and Fours", 7, 8): {8: 4625, 44: 40601, 48: 26475, 54: 28299}, + ("Category Double Threes and Fours", 8, 1): {0: 3982, 16: 56447, 28: 39571}, + ("Category Double Threes and Fours", 8, 2): {0: 1645, 20: 25350, 30: 37385, 42: 35620}, + ("Category Double Threes and Fours", 8, 3): {0: 6, 26: 23380, 40: 40181, 50: 36433}, + ("Category Double Threes and Fours", 8, 4): {0: 541, 20: 16547, 42: 38406, 52: 44506}, + ("Category Double Threes and Fours", 8, 5): {6: 2956, 30: 16449, 46: 43983, 56: 36612}, + ("Category Double Threes and Fours", 8, 6): {0: 2, 12: 7360, 38: 19332, 54: 53627, 58: 19679}, + ("Category Double Threes and Fours", 8, 7): {6: 9699, 48: 38611, 54: 28390, 60: 23300}, + ("Category Double Threes and Fours", 8, 8): {8: 5, 20: 10535, 52: 41790, 62: 47670}, + ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 8: 33433}, + ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 8: 55191}, + ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 8: 62900}, + ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 8: 69037}, + ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 8: 74684}, + ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 8: 78495}, + ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 8: 82324}, + ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 8: 85029}, + ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 8: 55434}, + ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 8: 57152, 12: 22885}, + ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 8: 52065, 16: 34169}, + ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 8: 46446, 16: 44011}, + ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 8: 40772, 16: 52756}, + ("Category Quadruple Ones and Twos", 2, 6): {0: 10306, 12: 46932, 16: 42762}, + ("Category Quadruple Ones and Twos", 2, 7): {0: 7120, 12: 42245, 16: 50635}, + ("Category Quadruple Ones and Twos", 2, 8): {0: 4989, 12: 37745, 16: 57266}, + ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 8: 50321, 16: 20239}, + ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 8: 42729, 16: 48414}, + ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 12: 53387, 20: 41550}, + ("Category Quadruple Ones and Twos", 3, 4): {0: 8395, 16: 64605, 24: 27000}, + ("Category Quadruple Ones and Twos", 3, 5): {0: 4895, 16: 58660, 24: 36445}, + ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 16: 52710, 24: 44609}, + ("Category Quadruple Ones and Twos", 3, 7): {0: 586, 16: 46781, 24: 52633}, + ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 16: 39406, 24: 59653}, + ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 8: 46945, 16: 33364}, + ("Category Quadruple Ones and Twos", 4, 2): {0: 4023, 12: 50885, 24: 45092}, + ("Category Quadruple Ones and Twos", 4, 3): {0: 6553, 16: 52095, 28: 41352}, + ("Category Quadruple Ones and Twos", 4, 4): {0: 3221, 16: 41367, 24: 39881, 28: 15531}, + ("Category Quadruple Ones and Twos", 4, 5): {0: 1561, 20: 48731, 28: 49708}, + ("Category Quadruple Ones and Twos", 4, 6): {0: 190, 20: 38723, 28: 42931, 32: 18156}, + ("Category Quadruple Ones and Twos", 4, 7): {0: 5419, 24: 53017, 32: 41564}, + ("Category Quadruple Ones and Twos", 4, 8): {0: 3135, 24: 47352, 32: 49513}, + ("Category Quadruple Ones and Twos", 5, 1): {0: 13112, 8: 41252, 20: 45636}, + ("Category Quadruple Ones and Twos", 5, 2): {0: 7293, 16: 50711, 28: 41996}, + ("Category Quadruple Ones and Twos", 5, 3): {0: 719, 20: 55921, 32: 43360}, + ("Category Quadruple Ones and Twos", 5, 4): {0: 1152, 20: 38570, 32: 60278}, + ("Category Quadruple Ones and Twos", 5, 5): {0: 5647, 24: 40910, 36: 53443}, + ("Category Quadruple Ones and Twos", 5, 6): {0: 194, 28: 51527, 40: 48279}, + ("Category Quadruple Ones and Twos", 5, 7): {0: 1449, 28: 39301, 36: 41332, 40: 17918}, + ("Category Quadruple Ones and Twos", 5, 8): {0: 6781, 32: 52834, 40: 40385}, + ("Category Quadruple Ones and Twos", 6, 1): {0: 8646, 12: 53753, 24: 37601}, + ("Category Quadruple Ones and Twos", 6, 2): {0: 844, 16: 40583, 28: 58573}, + ("Category Quadruple Ones and Twos", 6, 3): {0: 1241, 24: 54870, 36: 43889}, + ("Category Quadruple Ones and Twos", 6, 4): {0: 1745, 28: 53286, 40: 44969}, + ("Category Quadruple Ones and Twos", 6, 5): {0: 2076, 32: 56909, 44: 41015}, + ("Category Quadruple Ones and Twos", 6, 6): {0: 6827, 32: 39400, 44: 53773}, + ("Category Quadruple Ones and Twos", 6, 7): {0: 1386, 36: 49865, 48: 48749}, + ("Category Quadruple Ones and Twos", 6, 8): {0: 1841, 36: 38680, 44: 40600, 48: 18879}, + ("Category Quadruple Ones and Twos", 7, 1): {0: 5780, 12: 46454, 24: 47766}, + ("Category Quadruple Ones and Twos", 7, 2): {0: 6122, 20: 38600, 32: 55278}, + ("Category Quadruple Ones and Twos", 7, 3): {0: 2065, 28: 52735, 40: 45200}, + ("Category Quadruple Ones and Twos", 7, 4): {0: 1950, 32: 50270, 44: 47780}, + ("Category Quadruple Ones and Twos", 7, 5): {0: 2267, 36: 49235, 48: 48498}, + ("Category Quadruple Ones and Twos", 7, 6): {0: 2500, 40: 53934, 52: 43566}, + ("Category Quadruple Ones and Twos", 7, 7): {0: 6756, 44: 53730, 56: 39514}, + ("Category Quadruple Ones and Twos", 7, 8): {0: 3625, 44: 45159, 56: 51216}, + ("Category Quadruple Ones and Twos", 8, 1): {0: 11493, 16: 50043, 28: 38464}, + ("Category Quadruple Ones and Twos", 8, 2): {0: 136, 24: 47795, 36: 52069}, + ("Category Quadruple Ones and Twos", 8, 3): {0: 2744, 32: 51640, 48: 45616}, + ("Category Quadruple Ones and Twos", 8, 4): {0: 2293, 36: 45979, 48: 51728}, + ("Category Quadruple Ones and Twos", 8, 5): {0: 2181, 40: 44909, 52: 52910}, + ("Category Quadruple Ones and Twos", 8, 6): {4: 2266, 44: 44775, 56: 52959}, + ("Category Quadruple Ones and Twos", 8, 7): {8: 2344, 48: 50198, 60: 47458}, + ("Category Quadruple Ones and Twos", 8, 8): {8: 2808, 48: 37515, 56: 37775, 64: 21902}, ("Category Micro Straight", 1, 1): {0: 100000}, ("Category Micro Straight", 1, 2): {0: 100000}, ("Category Micro Straight", 1, 3): {0: 100000}, @@ -3527,7 +2329,7 @@ ("Category 4&5 Full House", 4, 6): {0: 100000}, ("Category 4&5 Full House", 4, 7): {0: 100000}, ("Category 4&5 Full House", 4, 8): {0: 100000}, - ("Category 4&5 Full House", 5, 1): {0: 99724, 50: 276}, + ("Category 4&5 Full House", 5, 1): {0: 100000}, ("Category 4&5 Full House", 5, 2): {0: 96607, 50: 3393}, ("Category 4&5 Full House", 5, 3): {0: 88788, 50: 11212}, ("Category 4&5 Full House", 5, 4): {0: 77799, 50: 22201}, @@ -3535,7 +2337,7 @@ ("Category 4&5 Full House", 5, 6): {0: 54548, 50: 45452}, ("Category 4&5 Full House", 5, 7): {0: 44898, 50: 55102}, ("Category 4&5 Full House", 5, 8): {0: 36881, 50: 63119}, - ("Category 4&5 Full House", 6, 1): {0: 98841, 50: 1159}, + ("Category 4&5 Full House", 6, 1): {0: 100000}, ("Category 4&5 Full House", 6, 2): {0: 88680, 50: 11320}, ("Category 4&5 Full House", 6, 3): {0: 70215, 50: 29785}, ("Category 4&5 Full House", 6, 4): {0: 50801, 50: 49199}, diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index c36c59544f15..75993fd39443 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -1,7 +1,7 @@ import math from typing import Dict -from BaseClasses import CollectionState, Entrance, Item, Region, Tutorial +from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region, Tutorial from worlds.AutoWorld import WebWorld, World @@ -56,7 +56,7 @@ class YachtDiceWorld(World): item_name_groups = item_groups - ap_world_version = "2.1.1" + ap_world_version = "2.1.3" def _get_yachtdice_data(self): return { @@ -190,7 +190,6 @@ def generate_early(self): 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 @@ def generate_early(self): 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 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) 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 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) 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 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) 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 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) # 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 @@ def get_item_to_add(weights, extra_points_added, multipliers_added, items_added) 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 @@ -464,16 +456,21 @@ def create_regions(self): if loc_data.region == board.name ] - # Add the victory item to the correct location. - # The website declares that the game is complete when the victory item is obtained. + # Change the victory location to an event and place the Victory item there. victory_location_name = f"{self.goal_score} score" - self.get_location(victory_location_name).place_locked_item(self.create_item("Victory")) + self.get_location(victory_location_name).address = None + self.get_location(victory_location_name).place_locked_item( + Item("Victory", ItemClassification.progression, None, self.player) + ) # add the regions connection = Entrance(self.player, "New Board", menu) menu.exits.append(connection) connection.connect(board) self.multiworld.regions += [menu, board] + + def get_filler_item_name(self) -> str: + return "Good RNG" def set_rules(self): """ diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 1cf44f090fed..a39b52cd09d5 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -430,7 +430,7 @@ def fill_slot_data(self) -> Dict[str, Any]: "final_campaign_boss_campaign_opponents": self.options.final_campaign_boss_campaign_opponents.value, "fourth_tier_5_campaign_boss_campaign_opponents": - self.options.fourth_tier_5_campaign_boss_unlock_condition.value, + self.options.fourth_tier_5_campaign_boss_campaign_opponents.value, "third_tier_5_campaign_boss_campaign_opponents": self.options.third_tier_5_campaign_boss_campaign_opponents.value, "number_of_challenges": self.options.number_of_challenges.value, diff --git a/worlds/yugioh06/rules.py b/worlds/yugioh06/rules.py index a804c7e7286a..0b46e0b5d0b0 100644 --- a/worlds/yugioh06/rules.py +++ b/worlds/yugioh06/rules.py @@ -39,10 +39,10 @@ def set_rules(world): "No Trap Cards Bonus": lambda state: yugioh06_difficulty(state, player, 2), "No Damage Bonus": lambda state: state.has_group("Campaign Boss Beaten", player, 3), "Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and - yugioh06_difficulty(state, player, 3), + yugioh06_difficulty(state, player, 2), "Extremely Low Deck Bonus": lambda state: state.has_any(["Reasoning", "Monster Gate", "Magical Merchant"], player) and - yugioh06_difficulty(state, player, 2), + yugioh06_difficulty(state, player, 3), "Opponent's Turn Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2), "Exactly 0 LP Bonus": lambda state: yugioh06_difficulty(state, player, 2), "Reversal Finish Bonus": lambda state: yugioh06_difficulty(state, player, 2), diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index cf61d93ca4ce..d5e86bb33292 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -4,7 +4,7 @@ import settings import threading import typing -from typing import Any, Dict, List, Set, Tuple, Optional +from typing import Any, Dict, List, Set, Tuple, Optional, Union import os import logging @@ -12,7 +12,7 @@ MultiWorld, Item, CollectionState, Entrance, Tutorial from .gen_data import GenData -from .logic import cs_to_zz_locs +from .logic import ZillionLogicCache from .region import ZillionLocation, ZillionRegion from .options import ZillionOptions, validate, z_option_groups from .id_maps import ZillionSlotInfo, get_slot_info, item_name_to_id as _item_name_to_id, \ @@ -21,7 +21,6 @@ from .item import ZillionItem from .patch import ZillionPatch -from zilliandomizer.randomizer import Randomizer as ZzRandomizer from zilliandomizer.system import System from zilliandomizer.logic_components.items import RESCUE, items as zz_items, Item as ZzItem from zilliandomizer.logic_components.locations import Location as ZzLocation, Req @@ -121,6 +120,7 @@ def flush(self) -> None: """ This is kind of a cache to avoid iterating through all the multiworld locations in logic. """ slot_data_ready: threading.Event """ This event is set in `generate_output` when the data is ready for `fill_slot_data` """ + logic_cache: Union[ZillionLogicCache, None] = None def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) @@ -134,9 +134,6 @@ def _make_item_maps(self, start_char: Chars) -> None: self.id_to_zz_item = id_to_zz_item def generate_early(self) -> None: - if not hasattr(self.multiworld, "zillion_logic_cache"): - setattr(self.multiworld, "zillion_logic_cache", {}) - zz_op, item_counts = validate(self.options) if zz_op.early_scope: @@ -163,6 +160,8 @@ def create_regions(self) -> None: assert self.zz_system.randomizer, "generate_early hasn't been called" assert self.id_to_zz_item, "generate_early hasn't been called" p = self.player + logic_cache = ZillionLogicCache(p, self.zz_system.randomizer, self.id_to_zz_item) + self.logic_cache = logic_cache w = self.multiworld self.my_locations = [] @@ -201,15 +200,12 @@ def create_regions(self) -> None: if not zz_loc.item: def access_rule_wrapped(zz_loc_local: ZzLocation, - p: int, - zz_r: ZzRandomizer, - id_to_zz_item: Dict[int, ZzItem], + lc: ZillionLogicCache, cs: CollectionState) -> bool: - accessible = cs_to_zz_locs(cs, p, zz_r, id_to_zz_item) + accessible = lc.cs_to_zz_locs(cs) return zz_loc_local in accessible - access_rule = functools.partial(access_rule_wrapped, - zz_loc, self.player, self.zz_system.randomizer, self.id_to_zz_item) + access_rule = functools.partial(access_rule_wrapped, zz_loc, logic_cache) loc_name = self.zz_system.randomizer.loc_name_2_pretty[zz_loc.name] loc = ZillionLocation(zz_loc, self.player, loc_name, here) @@ -402,13 +398,6 @@ def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot game = self.zz_system.get_game() return get_slot_info(game.regions, game.char_order[0], game.loc_name_2_pretty) - # def modify_multidata(self, multidata: Dict[str, Any]) -> None: - # """For deeper modification of server multidata.""" - # # not modifying multidata, just want to call this at the end of the generation process - # cache = getattr(self.multiworld, "zillion_logic_cache") - # import sys - # print(sys.getsizeof(cache)) - # end of ordered Main.py calls def create_item(self, name: str) -> Item: diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index dcbc6131f1a9..a14910a200e5 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,4 +1,4 @@ -from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter +from typing import Dict, FrozenSet, Mapping, Tuple, List, Counter as _Counter from BaseClasses import CollectionState @@ -44,38 +44,51 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id) -LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]] -""" { hash: (cs.prog_items, accessible_locations) } """ - - -def cs_to_zz_locs(cs: CollectionState, p: int, zz_r: Randomizer, id_to_zz_item: Dict[int, Item]) -> FrozenSet[Location]: - """ - given an Archipelago `CollectionState`, - returns frozenset of accessible zilliandomizer locations - """ - # caching this function because it would be slow - logic_cache: LogicCacheType = getattr(cs.multiworld, "zillion_logic_cache", {}) - _hash = set_randomizer_locs(cs, p, zz_r) - counts = item_counts(cs, p) - _hash += hash(counts) - - if _hash in logic_cache and logic_cache[_hash][0] == cs.prog_items: - # print("cache hit") - return logic_cache[_hash][1] - - # print("cache miss") - have_items: List[Item] = [] - for name, count in counts: - have_items.extend([id_to_zz_item[item_name_to_id[name]]] * count) - # have_req is the result of converting AP CollectionState to zilliandomizer collection state - have_req = zz_r.make_ability(have_items) - - # This `get_locations` is where the core of the logic comes in. - # It takes a zilliandomizer collection state (a set of the abilities that I have) - # and returns list of all the zilliandomizer locations I can access with those abilities. - tr = frozenset(zz_r.get_locations(have_req)) - - # save result in cache - logic_cache[_hash] = (cs.prog_items.copy(), tr) - - return tr +_cache_miss: Tuple[None, FrozenSet[Location]] = (None, frozenset()) + + +class ZillionLogicCache: + _cache: Dict[int, Tuple[_Counter[str], FrozenSet[Location]]] + """ `{ hash: (counter_from_prog_items, accessible_zz_locations) }` """ + _player: int + _zz_r: Randomizer + _id_to_zz_item: Mapping[int, Item] + + def __init__(self, player: int, zz_r: Randomizer, id_to_zz_item: Mapping[int, Item]) -> None: + self._cache = {} + self._player = player + self._zz_r = zz_r + self._id_to_zz_item = id_to_zz_item + + def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]: + """ + given an Archipelago `CollectionState`, + returns frozenset of accessible zilliandomizer locations + """ + # caching this function because it would be slow + _hash = set_randomizer_locs(cs, self._player, self._zz_r) + counts = item_counts(cs, self._player) + _hash += hash(counts) + + cntr, locs = self._cache.get(_hash, _cache_miss) + if cntr == cs.prog_items[self._player]: + # print("cache hit") + return locs + + # print("cache miss") + have_items: List[Item] = [] + for name, count in counts: + have_items.extend([self._id_to_zz_item[item_name_to_id[name]]] * count) + # have_req is the result of converting AP CollectionState to zilliandomizer collection state + have_req = self._zz_r.make_ability(have_items) + # print(f"{have_req=}") + + # This `get_locations` is where the core of the logic comes in. + # It takes a zilliandomizer collection state (a set of the abilities that I have) + # and returns list of all the zilliandomizer locations I can access with those abilities. + tr = frozenset(self._zz_r.get_locations(have_req)) + + # save result in cache + self._cache[_hash] = (cs.prog_items[self._player].copy(), tr) + + return tr