diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..5ab537933405 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +worlds/blasphemous/region_data.py linguist-generated=true +worlds/yachtdice/YachtWeights.py linguist-generated=true diff --git a/.github/pyright-config.json b/.github/pyright-config.json index 6ad7fa5f19b5..7d981778905f 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -16,7 +16,7 @@ "reportMissingImports": true, "reportMissingTypeStubs": true, - "pythonVersion": "3.8", + "pythonVersion": "3.10", "pythonPlatform": "Windows", "executionEnvironments": [ diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index c9995fa2d043..b59336fafe9b 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/setup-python@v5 if: env.diff != '' with: - python-version: 3.8 + python-version: '3.10' - name: "Install dependencies" if: env.diff != '' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23c463fb947a..27ca76e41f8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,14 +24,15 @@ env: jobs: # build-release-macos: # LF volunteer - build-win-py38: # RCs will still be built and signed by hand + build-win: # RCs will still be built and signed by hand runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Install python uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '~3.12.7' + check-latest: true - name: Download run-time dependencies run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip @@ -111,10 +112,11 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b0cfe35d2bc5..3abbb5f6449f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f8651d408e7..aec4f90998cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,10 +44,11 @@ jobs: - name: Get a recent python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '~3.12.7' + check-latest: true - name: Install build-time dependencies run: | - echo "PYTHON=python3.11" >> $GITHUB_ENV + echo "PYTHON=python3.12" >> $GITHUB_ENV wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml index 5234d862b4d3..ac842070625f 100644 --- a/.github/workflows/scan-build.yml +++ b/.github/workflows/scan-build.yml @@ -40,10 +40,10 @@ jobs: run: | wget https://apt.llvm.org/llvm.sh chmod +x ./llvm.sh - sudo ./llvm.sh 17 + sudo ./llvm.sh 19 - name: Install scan-build command run: | - sudo apt install clang-tools-17 + sudo apt install clang-tools-19 - name: Get a recent python uses: actions/setup-python@v5 with: @@ -56,7 +56,7 @@ jobs: - name: scan-build run: | source venv/bin/activate - scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y + scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y - name: Store report if: failure() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 3ad29b007772..88b5d12987ad 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -33,16 +33,15 @@ jobs: matrix: os: [ubuntu-latest] python: - - {version: '3.8'} - - {version: '3.9'} - {version: '3.10'} - {version: '3.11'} + - {version: '3.12'} include: - - python: {version: '3.8'} # win7 compat + - python: {version: '3.10'} # old 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 +69,7 @@ jobs: os: - ubuntu-latest python: - - {version: '3.11'} # current + - {version: '3.12'} # current steps: - uses: actions/checkout@v4 @@ -88,4 +87,4 @@ jobs: run: | source venv/bin/activate export PYTHONPATH=$(pwd) - python test/hosting/__main__.py + timeout 600 python test/hosting/__main__.py diff --git a/.gitignore b/.gitignore index 5686f43de380..791f7b1bb7fe 100644 --- a/.gitignore +++ b/.gitignore @@ -150,7 +150,7 @@ venv/ ENV/ env.bak/ venv.bak/ -.code-workspace +*.code-workspace shell.nix # Spyder project settings diff --git a/BaseClasses.py b/BaseClasses.py index 88857f803212..98ada4f861ec 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,37 +1,37 @@ from __future__ import annotations -import copy -import itertools +import collections import functools import logging import random import secrets -import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag -from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \ - TypedDict, Union, Type, ClassVar +from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, + Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) + +from typing_extensions import NotRequired, TypedDict import NetUtils import Options import Utils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from worlds import AutoWorld -class Group(TypedDict, total=False): +class Group(TypedDict): name: str game: str world: "AutoWorld.World" - players: Set[int] - item_pool: Set[str] - replacement_items: Dict[int, Optional[str]] - local_items: Set[str] - non_local_items: Set[str] - link_replacement: bool + players: AbstractSet[int] + item_pool: NotRequired[Set[str]] + replacement_items: NotRequired[Dict[int, Optional[str]]] + local_items: NotRequired[Set[str]] + non_local_items: NotRequired[Set[str]] + link_replacement: NotRequired[bool] class ThreadBarrierProxy: @@ -48,6 +48,11 @@ def __getattr__(self, name: str) -> Any: "Please use multiworld.per_slot_randoms[player] or randomize ahead of output.") +class HasNameAndPlayer(Protocol): + name: str + player: int + + class MultiWorld(): debug_types = False player_name: Dict[int, str] @@ -63,7 +68,6 @@ class MultiWorld(): state: CollectionState plando_options: PlandoOptions - accessibility: Dict[int, Options.Accessibility] early_items: Dict[int, Dict[str, int]] local_early_items: Dict[int, Dict[str, int]] local_items: Dict[int, Options.LocalItems] @@ -157,7 +161,7 @@ def __init__(self, players: int): self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} for player in range(1, players + 1): - def set_player_attr(attr, val): + def set_player_attr(attr: str, val) -> None: self.__dict__.setdefault(attr, {})[player] = val set_player_attr('plando_items', []) set_player_attr('plando_texts', {}) @@ -166,13 +170,13 @@ def set_player_attr(attr, val): set_player_attr('completion_condition', lambda state: True) self.worlds = {} self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the " - "world's random object instead (usually self.random)") + "world's random object instead (usually self.random)") self.plando_options = PlandoOptions.none def get_all_ids(self) -> Tuple[int, ...]: return self.player_ids + tuple(self.groups) - def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]: + def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]: """Create a group with name and return the assigned player ID and group. If a group of this name already exists, the set of players is extended instead of creating a new one.""" from worlds import AutoWorld @@ -188,7 +192,9 @@ def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tu 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, @@ -196,7 +202,7 @@ def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tu return new_id, new_group - def get_player_groups(self, player) -> Set[int]: + def get_player_groups(self, player: int) -> Set[int]: return {group_id for group_id, group in self.groups.items() if player in group["players"]} def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None): @@ -223,7 +229,7 @@ def set_options(self, args: Namespace) -> None: for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass + options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) @@ -259,7 +265,7 @@ def set_item_links(self): "link_replacement": replacement_prio.index(item_link["link_replacement"]), } - for name, item_link in item_links.items(): + for _name, item_link in item_links.items(): current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups pool = set() local_items = set() @@ -288,6 +294,88 @@ def set_item_links(self): group["non_local_items"] = item_link["non_local_items"] group["link_replacement"] = replacement_prio[item_link["link_replacement"]] + def link_items(self) -> None: + """Called to link together items in the itempool related to the registered item link groups.""" + from worlds import AutoWorld + + for group_id, group in self.groups.items(): + def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ + Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] + ]: + classifications: Dict[str, int] = collections.defaultdict(int) + counters = {player: {name: 0 for name in shared_pool} for player in players} + for item in self.itempool: + if item.player in counters and item.name in shared_pool: + counters[item.player][item.name] += 1 + classifications[item.name] |= item.classification + + for player in players.copy(): + if all([counters[player][item] == 0 for item in shared_pool]): + players.remove(player) + del (counters[player]) + + if not players: + return None, None + + for item in shared_pool: + count = min(counters[player][item] for player in players) + if count: + for player in players: + counters[player][item] = count + else: + for player in players: + del (counters[player][item]) + return counters, classifications + + common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) + if not common_item_count: + continue + + new_itempool: List[Item] = [] + for item_name, item_count in next(iter(common_item_count.values())).items(): + for _ in range(item_count): + new_item = group["world"].create_item(item_name) + # mangle together all original classification bits + new_item.classification |= classifications[item_name] + new_itempool.append(new_item) + + region = Region(group["world"].origin_region_name, 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: + loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}", + None, region) + loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ + state.has(item_name, group_id_, count_) + + locations.append(loc) + loc.place_locked_item(item) + common_item_count[item.player][item.name] -= 1 + else: + new_itempool.append(item) + + itemcount = len(self.itempool) + self.itempool = new_itempool + + while itemcount > len(self.itempool): + items_to_add = [] + for player in group["players"]: + if group["link_replacement"]: + item_player = group_id + else: + item_player = player + if group["replacement_items"][player]: + items_to_add.append(AutoWorld.call_single(self, "create_item", item_player, + group["replacement_items"][player])) + else: + items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player)) + self.random.shuffle(items_to_add) + self.itempool.extend(items_to_add[:itemcount - len(self.itempool)]) + def secure(self): self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True @@ -309,7 +397,7 @@ def get_game_worlds(self, game_name: str): return tuple(world for player, world in self.worlds.items() if player not in self.groups and self.game[player] == game_name) - def get_name_string_for_object(self, obj) -> str: + def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str: return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})' def get_player_name(self, player: int) -> str: @@ -351,7 +439,7 @@ def get_all_state(self, use_cache: bool) -> CollectionState: subworld = self.worlds[player] for item in subworld.get_pre_fill_items(): subworld.collect(ret, item) - ret.sweep_for_events() + ret.sweep_for_advancements() if use_cache: self._all_state = ret @@ -360,7 +448,7 @@ def get_all_state(self, use_cache: bool) -> CollectionState: def get_items(self) -> List[Item]: return [loc.item for loc in self.get_filled_locations()] + self.itempool - def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]: + def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]: if resolve_group_locations: player_groups = self.get_player_groups(player) return [location for location in self.get_locations() if @@ -369,7 +457,7 @@ def find_item_locations(self, item, player: int, resolve_group_locations: bool = return [location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player] - def find_item(self, item, player: int) -> Location: + def find_item(self, item: str, player: int) -> Location: return next(location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player) @@ -462,9 +550,9 @@ def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> boo return True state = starting_state.copy() else: - if self.has_beaten_game(self.state): - return True state = CollectionState(self) + if self.has_beaten_game(state): + return True prog_locations = {location for location in self.get_locations() if location.item and location.item.advancement and location not in state.locations_checked} @@ -516,6 +604,49 @@ def get_spheres(self) -> Iterator[Set[Location]]: state.collect(location.item, True, location) locations -= sphere + def get_sendable_spheres(self) -> Iterator[Set[Location]]: + """ + yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere + + If there are unreachable locations, the last sphere of reachable locations is followed by an empty set, + and then a set of all of the unreachable locations. + """ + state = CollectionState(self) + locations: Set[Location] = set() + events: Set[Location] = set() + for location in self.get_filled_locations(): + if type(location.item.code) is int: + locations.add(location) + else: + events.add(location) + + while locations: + sphere: Set[Location] = set() + + # cull events out + done_events: Set[Union[Location, None]] = {None} + while done_events: + done_events = set() + for event in events: + if event.can_reach(state): + state.collect(event.item, True, event) + done_events.add(event) + events -= done_events + + for location in locations: + if location.can_reach(state): + sphere.add(location) + + yield sphere + if not sphere: + if locations: + yield locations # unreachable locations + break + + for location in sphere: + state.collect(location.item, True, location) + locations -= sphere + def fulfills_accessibility(self, state: Optional[CollectionState] = None): """Check if accessibility rules are fulfilled with current or supplied state.""" if not state: @@ -523,26 +654,21 @@ def fulfills_accessibility(self, state: Optional[CollectionState] = None): players: Dict[str, Set[int]] = { "minimal": set(), "items": set(), - "locations": set() + "full": set() } - for player, access in self.accessibility.items(): - players[access.current_key].add(player) + for player, world in self.worlds.items(): + players[world.options.accessibility.current_key].add(player) beatable_fulfilled = False - def location_condition(location: Location): + def location_condition(location: Location) -> bool: """Determine if this location has to be accessible, location is already filtered by location_relevant""" - if location.player in players["locations"] or (location.item and location.item.player not in - players["minimal"]): - return True - return False + return location.player in players["full"] or \ + (location.item and location.item.player not in players["minimal"]) - def location_relevant(location: Location): + def location_relevant(location: Location) -> bool: """Determine if this location is relevant to sweep.""" - if location.progress_type != LocationProgressType.EXCLUDED \ - and (location.player in players["locations"] or location.advancement): - return True - return False + return location.player in players["full"] or location.advancement def all_done() -> bool: """Check if all access rules are fulfilled""" @@ -587,7 +713,7 @@ class CollectionState(): multiworld: MultiWorld reachable_regions: Dict[int, Set[Region]] blocked_connections: Dict[int, Set[Entrance]] - events: Set[Location] + advancements: Set[Location] path: Dict[Union[Region, Entrance], PathValue] locations_checked: Set[Location] stale: Dict[int, bool] @@ -599,7 +725,7 @@ def __init__(self, parent: MultiWorld): self.multiworld = parent self.reachable_regions = {player: set() for player in parent.get_all_ids()} self.blocked_connections = {player: set() for player in parent.get_all_ids()} - self.events = set() + self.advancements = set() self.path = {} self.locations_checked = set() self.stale = {player: True for player in parent.get_all_ids()} @@ -611,17 +737,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() @@ -629,7 +763,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) @@ -641,16 +775,39 @@ 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 = copy.deepcopy(self.prog_items) - ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in - self.reachable_regions} - ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in - self.blocked_connections} - ret.events = copy.copy(self.events) - ret.path = copy.copy(self.path) - ret.locations_checked = copy.copy(self.locations_checked) + ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()} + ret.reachable_regions = {player: region_set.copy() for player, region_set in + self.reachable_regions.items()} + ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in + self.blocked_connections.items()} + ret.advancements = self.advancements.copy() + ret.path = self.path.copy() + ret.locations_checked = self.locations_checked.copy() for function in self.additional_copy_functions: ret = function(self, ret) return ret @@ -680,20 +837,25 @@ def can_reach_entrance(self, spot: str, player: int) -> bool: def can_reach_region(self, spot: str, player: int) -> bool: return self.multiworld.get_region(spot, player).can_reach(self) - def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None: + def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None: + Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. " + "Please switch over to sweep_for_advancements.") + return self.sweep_for_advancements(locations) + + def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None: if locations is None: locations = self.multiworld.get_filled_locations() - reachable_events = True - # since the loop has a good chance to run more than once, only filter the events once - locations = {location for location in locations if location.advancement and location not in self.events and - not key_only or getattr(location.item, "locked_dungeon_item", False)} - while reachable_events: - reachable_events = {location for location in locations if location.can_reach(self)} - locations -= reachable_events - for event in reachable_events: - self.events.add(event) - assert isinstance(event.item, Item), "tried to collect Event with no Item" - self.collect(event.item, True, event) + reachable_advancements = True + # since the loop has a good chance to run more than once, only filter the advancements once + locations = {location for location in locations if location.advancement and location not in self.advancements} + + while reachable_advancements: + reachable_advancements = {location for location in locations if location.can_reach(self)} + locations -= reachable_advancements + for advancement in reachable_advancements: + self.advancements.add(advancement) + assert isinstance(advancement.item, Item), "tried to collect Event with no Item" + self.collect(advancement.item, True, advancement) # item name related def has(self, item: str, player: int, count: int = 1) -> bool: @@ -727,7 +889,7 @@ def has_from_list(self, items: Iterable[str], player: int, count: int) -> bool: if found >= count: return True return False - + def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool: """Returns True if the state contains at least `count` items matching any of the item names from a list. Ignores duplicates of the same item.""" @@ -742,7 +904,7 @@ def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> def count_from_list(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state.""" return sum(self.prog_items[player][item_name] for item_name in items) - + def count_from_list_unique(self, items: Iterable[str], player: int) -> int: """Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item.""" return sum(self.prog_items[player][item_name] > 0 for item_name in items) @@ -788,20 +950,16 @@ def count_group_unique(self, item_name_group: str, player: int) -> int: ) # Item related - def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: + def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) changed = self.multiworld.worlds[item.player].collect(self, item) - if not changed and event: - self.prog_items[item.player][item.name] += 1 - changed = True - self.stale[item.player] = True - if changed and not event: - self.sweep_for_events() + if changed and not prevent_sweep: + self.sweep_for_advancements() return changed @@ -825,12 +983,13 @@ class Entrance: addresses = None target = None - def __init__(self, player: int, name: str = '', parent: Region = None): + def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None: self.name = name self.parent_region = parent 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))) @@ -845,9 +1004,6 @@ def connect(self, region: Region, addresses: Any = None, target: Any = None) -> region.entrances.append(self) def __repr__(self): - return self.__str__() - - def __str__(self): multiworld = self.parent_region.multiworld if self.parent_region else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -860,7 +1016,7 @@ class Region: entrances: List[Entrance] exits: List[Entrance] locations: List[Location] - entrance_type: ClassVar[Type[Entrance]] = Entrance + entrance_type: ClassVar[type[Entrance]] = Entrance class Register(MutableSequence): region_manager: MultiWorld.RegionManager @@ -960,7 +1116,7 @@ def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) return entrance.parent_region.get_connecting_entrance(is_main_entrance) def add_locations(self, locations: Dict[str, Optional[int]], - location_type: Optional[Type[Location]] = None) -> None: + location_type: Optional[type[Location]] = None) -> None: """ Adds locations to the Region object, where location_type is your Location class and locations is a dict of location names to address. @@ -973,7 +1129,7 @@ def add_locations(self, locations: Dict[str, Optional[int]], self.locations.append(location_type(self.player, location, address, self)) def connect(self, connecting_region: Region, name: Optional[str] = None, - rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type: + rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -997,7 +1153,7 @@ def create_exit(self, name: str) -> Entrance: return exit_ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]], - rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None: + rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]: """ Connects current region to regions in exit dictionary. Passed region names must exist first. @@ -1007,15 +1163,16 @@ def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]], """ if not isinstance(exits, Dict): exits = dict.fromkeys(exits) - for connecting_region, name in exits.items(): - self.connect(self.multiworld.get_region(connecting_region, self.player), - name, - rules[connecting_region] if rules and connecting_region in rules else None) + return [ + self.connect( + self.multiworld.get_region(connecting_region, self.player), + name, + rules[connecting_region] if rules and connecting_region in rules else None, + ) + for connecting_region, name in exits.items() + ] def __repr__(self): - return self.__str__() - - def __str__(self): return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})' @@ -1034,9 +1191,9 @@ class Location: locked: bool = False show_in_spoiler: bool = True progress_type: LocationProgressType = LocationProgressType.DEFAULT - always_allow = staticmethod(lambda state, item: False) + always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False) access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True) - item_rule = staticmethod(lambda item: True) + item_rule: Callable[[Item], bool] = staticmethod(lambda item: True) item: Optional[Item] = None def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): @@ -1045,16 +1202,20 @@ def __init__(self, player: int, name: str = '', address: Optional[int] = None, p self.address = address self.parent_region = parent - def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items) - or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) - and self.item_rule(item) - and (not check_access or self.can_reach(state)))) + def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: + return (( + self.always_allow(state, item) + and item.name not in state.multiworld.worlds[item.player].options.non_local_items + ) or ( + (self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful)) + and self.item_rule(item) + and (not check_access or self.can_reach(state)) + )) def can_reach(self, state: CollectionState) -> bool: - # self.access_rule computes faster on average, so placing it first for faster abort - assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and self.parent_region.can_reach(state) + # Region.can_reach is just a cache lookup, so placing it first for faster abort on average + 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): if self.item: @@ -1064,9 +1225,6 @@ def place_locked_item(self, item: Item): self.locked = True def __repr__(self): - return self.__str__() - - def __str__(self): multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})' @@ -1088,7 +1246,7 @@ def is_event(self) -> bool: @property def native_item(self) -> bool: """Returns True if the item in this location matches game.""" - return self.item and self.item.game == self.game + return self.item is not None and self.item.game == self.game @property def hint_text(self) -> str: @@ -1099,7 +1257,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. @@ -1151,6 +1309,14 @@ def useful(self) -> bool: def trap(self) -> bool: return ItemClassification.trap in self.classification + @property + def filler(self) -> bool: + return not (self.advancement or self.useful or self.trap) + + @property + def excludable(self) -> bool: + return not (self.advancement or self.useful) + @property def flags(self) -> int: return self.classification.as_flag() @@ -1171,9 +1337,6 @@ def __hash__(self) -> int: return hash((self.name, self.player)) def __repr__(self) -> str: - return self.__str__() - - def __str__(self) -> str: if self.location and self.location.parent_region and self.location.parent_region.multiworld: return self.location.parent_region.multiworld.get_name_string_for_object(self) return f"{self.name} (Player {self.player})" @@ -1251,9 +1414,9 @@ def create_playthrough(self, create_paths: bool = True) -> None: # in the second phase, we cull each sphere such that the game is still beatable, # reducing each range of influence to the bare minimum required inside it - restore_later = {} + restore_later: Dict[Location, Item] = {} for num, sphere in reversed(tuple(enumerate(collection_spheres))): - to_delete = set() + to_delete: Set[Location] = set() for location in sphere: # we remove the item at location and check if game is still beatable logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, @@ -1271,15 +1434,22 @@ def create_playthrough(self, create_paths: bool = True) -> None: sphere -= to_delete # second phase, sphere 0 - removed_precollected = [] - for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement): - logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) - multiworld.precollected_items[item.player].remove(item) - multiworld.state.remove(item) - if not multiworld.can_beat_game(): - multiworld.push_precollected(item) - else: - removed_precollected.append(item) + removed_precollected: List[Item] = [] + + for precollected_items in multiworld.precollected_items.values(): + # The list of items is mutated by removing one item at a time to determine if each item is required to beat + # the game, and re-adding that item if it was required, so a copy needs to be made before iterating. + for item in precollected_items.copy(): + if not item.advancement: + continue + logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) + precollected_items.remove(item) + multiworld.state.remove(item) + if not multiworld.can_beat_game(): + # Add the item back into `precollected_items` and collect it into `multiworld.state`. + multiworld.push_precollected(item) + else: + removed_precollected.append(item) # we are now down to just the required progress items in collection_spheres. Unfortunately # the previous pruning stage could potentially have made certain items dependant on others @@ -1291,8 +1461,6 @@ def create_playthrough(self, create_paths: bool = True) -> None: state = CollectionState(multiworld) collection_spheres = [] while required_locations: - state.sweep_for_events(key_only=True) - sphere = set(filter(state.can_reach, required_locations)) for location in sphere: @@ -1354,7 +1522,7 @@ def get_path(state: CollectionState, region: Region) -> List[Union[Tuple[str, st # Maybe move the big bomb over to the Event system instead? if any(exit_path == 'Pyramid Fairy' for path in self.paths.values() for (_, exit_path) in path): - if multiworld.mode[player] != 'inverted': + if multiworld.worlds[player].options.mode != 'inverted': self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \ get_path(state, multiworld.get_region('Big Bomb Shop', player)) else: @@ -1420,15 +1588,15 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None: [f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else [f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()])) if self.unreachables: - outfile.write('\n\nUnreachable Items:\n\n') + outfile.write('\n\nUnreachable Progression Items:\n\n') outfile.write( '\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) if self.paths: outfile.write('\n\nPaths:\n\n') - path_listings = [] + path_listings: List[str] = [] for location, path in sorted(self.paths.items()): - path_lines = [] + path_lines: List[str] = [] for region, exit in path: if exit is not None: path_lines.append("{} -> {}".format(region, exit)) 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 f8d1fcb7a221..fc6ae6d9a5fa 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -23,7 +23,7 @@ from MultiServer import CommandProcessor from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot, - RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType) + RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType) from Utils import Version, stream_input, async_start from worlds import network_data_package, AutoWorldRegister import os @@ -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: @@ -61,6 +72,7 @@ def _cmd_connect(self, address: str = "") -> bool: if address: self.ctx.server_address = None self.ctx.username = None + self.ctx.password = None elif not self.ctx.server_address: self.output("Please specify an address.") return False @@ -163,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 @@ -251,7 +264,7 @@ def update_game(self, game: str, name_to_id_lookup_table: typing.Dict[str, int]) starting_reconnect_delay: int = 5 current_reconnect_delay: int = starting_reconnect_delay command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor - ui = None + ui: typing.Optional["kvui.GameManager"] = None ui_task: typing.Optional["asyncio.Task[None]"] = None input_task: typing.Optional["asyncio.Task[None]"] = None keep_alive_task: typing.Optional["asyncio.Task[None]"] = None @@ -342,6 +355,8 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing self.item_names = self.NameLookupDict(self, "item") self.location_names = self.NameLookupDict(self, "location") + self.versions = {} + self.checksums = {} self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) @@ -397,6 +412,7 @@ async def disconnect(self, allow_autoreconnect: bool = False): await self.server.socket.close() if self.server_task is not None: await self.server_task + self.ui.update_hints() async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: """ `msgs` JSON serializable """ @@ -428,7 +444,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, @@ -438,6 +457,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None: if kwargs: payload.update(kwargs) await self.send_msgs([payload]) + await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}]) async def console_input(self) -> str: if self.ui: @@ -458,6 +478,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: @@ -465,6 +486,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 @@ -496,13 +518,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) @@ -514,6 +537,7 @@ def update_permissions(self, permissions: typing.Dict[str, int]): async def shutdown(self): self.server_address = "" self.username = None + self.password = None self.cancel_autoreconnect() if self.server and not self.server.socket.closed: await self.server.socket.close() @@ -528,7 +552,14 @@ async def shutdown(self): await self.ui_task if self.input_task: self.input_task.cancel() - + + # Hints + def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None: + msg = {"cmd": "UpdateHint", "location": location, "player": finding_player} + if status is not None: + msg["status"] = status + async_start(self.send_msgs([msg]), name="update_hint") + # DataPackage async def prepare_data_package(self, relevant_games: typing.Set[str], remote_date_package_versions: typing.Dict[str, int], @@ -550,26 +581,34 @@ async def prepare_data_package(self, relevant_games: typing.Set[str], needed_updates.add(game) continue - local_version: int = network_data_package["games"].get(game, {}).get("version", 0) - local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") - # no action required if local version is new enough - if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ - or remote_checksum != local_checksum: - cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) - cache_version: int = cached_game.get("version", 0) - cache_checksum: typing.Optional[str] = cached_game.get("checksum") - # download remote version if cache is not new enough - if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ - or remote_checksum != cache_checksum: - needed_updates.add(game) + cached_version: int = self.versions.get(game, 0) + cached_checksum: typing.Optional[str] = self.checksums.get(game) + # no action required if cached version is new enough + if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \ + or remote_checksum != cached_checksum: + local_version: int = network_data_package["games"].get(game, {}).get("version", 0) + local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") + if ((remote_checksum or remote_version <= local_version and remote_version != 0) + and remote_checksum == local_checksum): + self.update_game(network_data_package["games"][game], game) else: - self.update_game(cached_game, game) + cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) + cache_version: int = cached_game.get("version", 0) + cache_checksum: typing.Optional[str] = cached_game.get("checksum") + # download remote version if cache is not new enough + if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ + or remote_checksum != cache_checksum: + needed_updates.add(game) + else: + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) def update_game(self, game_package: dict, game: str): self.item_names.update_game(game, game_package["item_name_to_id"]) self.location_names.update_game(game, game_package["location_name_to_id"]) + self.versions[game] = game_package.get("version", 0) + self.checksums[game] = game_package.get("checksum") def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): @@ -611,6 +650,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() @@ -624,6 +664,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") @@ -633,7 +674,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" @@ -660,21 +701,28 @@ 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): if sys.stdin: + if sys.stdin.fileno() != 0: + from multiprocessing import parent_process + if parent_process(): + return # ignore MultiProcessing pipe + # steam overlay breaks when starting console_loop if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") @@ -983,6 +1031,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.') @@ -992,7 +1041,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"} @@ -1031,16 +1080,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)) @@ -1049,4 +1103,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 4967ff073601..86a4639c51ce 100644 --- a/Fill.py +++ b/Fill.py @@ -12,7 +12,12 @@ class FillError(RuntimeError): - pass + def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None: + if "multiworld" in kwargs and isinstance(args[0], str): + placements = (args[0] + f"\nAll Placements:\n" + + f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}") + args = (placements, *args[1:]) + super().__init__(*args) def _log_fill_progress(name: str, placed: int, total_items: int) -> None: @@ -24,14 +29,15 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] new_state = base_state.copy() for item in itempool: new_state.collect(item, True) - new_state.sweep_for_events(locations=locations) + new_state.sweep_for_advancements(locations=locations) return new_state def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location], item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None: + allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True, + name: str = "Unknown") -> None: """ :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. @@ -58,14 +64,22 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati placed = 0 while any(reachable_items.values()) and locations: - # grab one item per player - items_to_place = [items.pop() - for items in reachable_items.values() if items] + if one_item_per_player: + # grab one item per player + items_to_place = [items.pop() + for items in reachable_items.values() if items] + else: + next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items]) + items_to_place = [] + if item_pool: + items_to_place.append(reachable_items[next_player].pop()) + for item in items_to_place: for p, pool_item in enumerate(item_pool): if pool_item is item: item_pool.pop(p) break + maximum_exploration_state = sweep_from_pool( base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player) if single_player_placement else None) @@ -212,7 +226,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) item_pool.extend(unplaced_items) @@ -299,7 +313,7 @@ def remaining_fill(multiworld: MultiWorld, f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) itempool.extend(unplaced_items) @@ -324,8 +338,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo pool.append(location.item) state.remove(location.item) location.item = None - if location in state.events: - state.events.remove(location) + if location in state.advancements: + state.advancements.remove(location) locations.append(location) if pool and locations: locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) @@ -358,7 +372,7 @@ def distribute_early_items(multiworld: MultiWorld, early_priority_locations: typing.List[Location] = [] loc_indexes_to_remove: typing.Set[int] = set() base_state = multiworld.state.copy() - base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) + base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None)) for i, loc in enumerate(fill_locations): if loc.can_reach(base_state): if loc.progress_type == LocationProgressType.PRIORITY: @@ -470,28 +484,27 @@ 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", one_item_per_player=False) 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.") @@ -506,7 +519,8 @@ def mark_for_locking(location: Location): if progitempool: raise FillError( f"Not enough locations for progression items. " - f"There are {len(progitempool)} more progression items than there are available locations." + f"There are {len(progitempool)} more progression items than there are available locations.", + multiworld=multiworld, ) accessibility_corrections(multiworld, multiworld.state, defaultlocations) @@ -523,7 +537,8 @@ def mark_for_locking(location: Location): if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " - f"There are {len(excludedlocations)} more excluded locations than filler or trap items." + f"There are {len(excludedlocations)} more excluded locations than filler or trap items.", + multiworld=multiworld, ) restitempool = filleritempool + usefulitempool @@ -551,7 +566,7 @@ def flood_items(multiworld: MultiWorld) -> None: progress_done = False # sweep once to pick up preplaced items - multiworld.state.sweep_for_events() + multiworld.state.sweep_for_advancements() # fill multiworld from top of itempool while we can while not progress_done: @@ -589,7 +604,7 @@ def flood_items(multiworld: MultiWorld) -> None: if candidate_item_to_place is not None: item_to_place = candidate_item_to_place else: - raise FillError('No more progress items left to place.') + raise FillError('No more progress items left to place.', multiworld=multiworld) # find item to replace with progress item location_list = multiworld.get_reachable_locations() @@ -646,7 +661,6 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None: def get_sphere_locations(sphere_state: CollectionState, locations: typing.Set[Location]) -> typing.Set[Location]: - sphere_state.sweep_for_events(key_only=True, locations=locations) return {loc for loc in locations if sphere_state.can_reach(loc)} def item_percentage(player: int, num: int) -> float: @@ -740,7 +754,7 @@ def item_percentage(player: int, num: int) -> float: ), items_to_test): reducing_state.collect(location.item, True, location) - reducing_state.sweep_for_events(locations=locations_to_test) + reducing_state.sweep_for_advancements(locations=locations_to_test) if multiworld.has_beaten_game(balancing_state): if not multiworld.has_beaten_game(reducing_state): @@ -823,7 +837,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: warn(warning, force) swept_state = multiworld.state.copy() - swept_state.sweep_for_events() + swept_state.sweep_for_advancements() reachable = frozenset(multiworld.get_reachable_locations(swept_state)) early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list) @@ -974,15 +988,32 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: multiworld.random.shuffle(items) count = 0 err: typing.List[str] = [] - successful_pairs: typing.List[typing.Tuple[Item, Location]] = [] + successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = [] + claimed_indices: typing.Set[typing.Optional[int]] = set() for item_name in items: - item = multiworld.worlds[player].create_item(item_name) + index_to_delete: typing.Optional[int] = None + if from_pool: + try: + # If from_pool, try to find an existing item with this name & player in the itempool and use it + index_to_delete, item = next( + (i, item) for i, item in enumerate(multiworld.itempool) + if item.player == player and item.name == item_name and i not in claimed_indices + ) + except StopIteration: + warn( + f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.", + placement['force']) + item = multiworld.worlds[player].create_item(item_name) + else: + item = multiworld.worlds[player].create_item(item_name) + for location in reversed(candidates): if (location.address is None) == (item.code is None): # either both None or both not None if not location.item: if location.item_rule(item): if location.can_fill(multiworld.state, item, False): - successful_pairs.append((item, location)) + successful_pairs.append((index_to_delete, item, location)) + claimed_indices.add(index_to_delete) candidates.remove(location) count = count + 1 break @@ -994,6 +1025,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: err.append(f"Cannot place {item_name} into already filled location {location}.") else: err.append(f"Mismatch between {item_name} and {location}, only one is an event.") + if count == maxcount: break if count < placement['count']['min']: @@ -1001,17 +1033,16 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None: failed( f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}", placement['force']) - for (item, location) in successful_pairs: + + # Sort indices in reverse so we can remove them one by one + successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True) + + for (index, item, location) in successful_pairs: multiworld.push_item(location, item, collect=False) location.locked = True logging.debug(f"Plando placed {item} at {location}") - if from_pool: - try: - multiworld.itempool.remove(item) - except ValueError: - warn( - f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.", - placement['force']) + if index is not None: # If this item is from_pool and was found in the pool, remove it. + multiworld.itempool.pop(index) except Exception as e: raise Exception( diff --git a/Generate.py b/Generate.py index d7dd6523e7f1..35c39627b139 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", @@ -110,11 +110,18 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: player_files = {} for file in os.scandir(args.player_files_path): fname = file.name - if file.is_file() and not fname.startswith(".") and \ + if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \ os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: path = os.path.join(args.player_files_path, fname) try: - weights_cache[fname] = read_weights_yamls(path) + weights_for_file = [] + for doc_idx, yaml in enumerate(read_weights_yamls(path)): + if yaml is None: + logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}") + else: + weights_for_file.append(yaml) + weights_cache[fname] = tuple(weights_for_file) + except Exception as e: raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e @@ -155,6 +162,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 +211,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 +224,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 @@ -473,6 +460,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) + if not isinstance(ret.game, str): + if ret.game is None: + raise Exception('"game" not specified') + raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] @@ -511,7 +502,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b continue logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.") if PlandoOptions.items in plando_options: - ret.plando_items = game_weights.get("plando_items", []) + ret.plando_items = copy.deepcopy(game_weights.get("plando_items", [])) if ret.game == "A Link to the Past": roll_alttp_settings(ret, game_weights) diff --git a/KH1Client.py b/KH1Client.py new file mode 100644 index 000000000000..4c3ed501901b --- /dev/null +++ b/KH1Client.py @@ -0,0 +1,9 @@ +if __name__ == '__main__': + import ModuleUpdate + ModuleUpdate.update() + + import Utils + Utils.init_logging("KH1Client", exception_logger="Client") + + from worlds.kh1.Client import launch + launch() diff --git a/Launcher.py b/Launcher.py index e4b65be93a68..22c0944ab1a4 100644 --- a/Launcher.py +++ b/Launcher.py @@ -16,25 +16,27 @@ 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 - -import Utils -import settings -from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths +from typing import Callable, Optional, Sequence, Tuple, Union if __name__ == "__main__": import ModuleUpdate ModuleUpdate.update() -from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ - is_windows, is_macos, is_linux +import settings +import Utils +from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename, + user_path) +from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type def open_host_yaml(): - file = settings.get_settings().filename + s = settings.get_settings() + file = s.filename + s.save() assert file, "host.yaml missing" if is_linux: exe = which('sensible-editor') or which('gedit') or \ @@ -101,13 +103,71 @@ def update_settings(): Component("Open host.yaml", func=open_host_yaml), Component("Open Patch", func=open_patch), Component("Generate Template Options", func=generate_yamls), + Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")), Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), Component("Browse Files", func=browse_files), ]) -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 + + if client_component is None: + run_component(text_client_component, *launch_args) + return + + from kvui import App, Button, BoxLayout, Label, Window + + class Popup(App): + def __init__(self): + self.title = "Connect to Multiworld" + self.icon = r"data/icon.png" + super().__init__() + + def build(self): + layout = BoxLayout(orientation="vertical") + 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 _stop(self, *largs): + # see run_gui Launcher _stop comment for details + self.root_window.close() + super()._stop(*largs) + + 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: @@ -164,9 +224,8 @@ def launch(exe, in_terminal=False): def run_gui(): - from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget + from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage from kivy.core.window import Window - from kivy.uix.image import AsyncImage from kivy.uix.relativelayout import RelativeLayout class Launcher(App): @@ -177,7 +236,7 @@ class Launcher(App): _client_layout: Optional[ScrollBox] = None def __init__(self, ctx=None): - self.title = self.base_title + self.title = self.base_title + " " + Utils.__version__ self.ctx = ctx self.icon = r"data/icon.png" super().__init__() @@ -199,8 +258,8 @@ def build_button(component: Component) -> Widget: button.component = component button.bind(on_release=self.component_action) if component.icon != "icon": - image = AsyncImage(source=icon_paths[component.icon], - size=(38, 38), size_hint=(None, 1), pos=(5, 0)) + image = ApAsyncImage(source=icon_paths[component.icon], + size=(38, 38), size_hint=(None, 1), pos=(5, 0)) box_layout = RelativeLayout(size_hint_y=None, height=40) box_layout.add_widget(button) box_layout.add_widget(image) @@ -266,7 +325,7 @@ def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None if file and component: run_component(component, file) else: - logging.warning(f"unable to identify component for {filename}") + logging.warning(f"unable to identify component for {file}") def _stop(self, *largs): # ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm. @@ -299,20 +358,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 +385,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/LttPAdjuster.py b/LttPAdjuster.py index 9c5bd102440b..7e33a3d5efe8 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -14,7 +14,7 @@ from argparse import Namespace from concurrent.futures import as_completed, ThreadPoolExecutor from glob import glob -from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \ +from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \ IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage from tkinter.constants import DISABLED, NORMAL from urllib.parse import urlparse @@ -29,7 +29,8 @@ GAME_ALTTP = "A Link to the Past" - +WINDOW_MIN_HEIGHT = 525 +WINDOW_MIN_WIDTH = 425 class AdjusterWorld(object): def __init__(self, sprite_pool): @@ -242,16 +243,17 @@ def adjustGUI(): from argparse import Namespace from Utils import __version__ as MWVersion adjustWindow = Tk() + adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT) adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion) set_icon(adjustWindow) rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow) - bottomFrame2 = Frame(adjustWindow) + bottomFrame2 = Frame(adjustWindow, padx=8, pady=2) romFrame, romVar = get_rom_frame(adjustWindow) - romDialogFrame = Frame(adjustWindow) + romDialogFrame = Frame(adjustWindow, padx=8, pady=2) baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust') romVar2 = StringVar() romEntry2 = Entry(romDialogFrame, textvariable=romVar2) @@ -261,9 +263,9 @@ def RomSelect2(): romVar2.set(rom) romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2) - romDialogFrame.pack(side=TOP, expand=True, fill=X) - baseRomLabel2.pack(side=LEFT) - romEntry2.pack(side=LEFT, expand=True, fill=X) + romDialogFrame.pack(side=TOP, expand=False, fill=X) + baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8)) + romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romSelectButton2.pack(side=LEFT) def adjustRom(): @@ -331,12 +333,11 @@ def saveGUISettings(): messagebox.showinfo(title="Success", message="Settings saved to persistent storage") adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom) - rom_options_frame.pack(side=TOP) + rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True) adjustButton.pack(side=LEFT, padx=(5,5)) saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings) saveButton.pack(side=LEFT, padx=(5,5)) - bottomFrame2.pack(side=TOP, pady=(5,5)) tkinter_center_window(adjustWindow) @@ -576,7 +577,7 @@ def hide(self): def get_rom_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - romFrame = Frame(parent) + romFrame = Frame(parent, padx=8, pady=8) baseRomLabel = Label(romFrame, text='LttP Base Rom: ') romVar = StringVar(value=adjuster_settings.baserom) romEntry = Entry(romFrame, textvariable=romVar) @@ -596,20 +597,19 @@ def RomSelect(): romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect) baseRomLabel.pack(side=LEFT) - romEntry.pack(side=LEFT, expand=True, fill=X) + romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1) romSelectButton.pack(side=LEFT) - romFrame.pack(side=TOP, expand=True, fill=X) + romFrame.pack(side=TOP, fill=X) return romFrame, romVar def get_rom_options_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) - romOptionsFrame = LabelFrame(parent, text="Rom options") - romOptionsFrame.columnconfigure(0, weight=1) - romOptionsFrame.columnconfigure(1, weight=1) + romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8) + for i in range(5): - romOptionsFrame.rowconfigure(i, weight=1) + romOptionsFrame.rowconfigure(i, weight=0, pad=4) vars = Namespace() vars.MusicVar = IntVar() @@ -660,7 +660,7 @@ def SpriteSelect(): spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect) baseSpriteLabel.pack(side=LEFT) - spriteEntry.pack(side=LEFT) + spriteEntry.pack(side=LEFT, expand=True, fill=X) spriteSelectButton.pack(side=LEFT) oofDialogFrame = Frame(romOptionsFrame) diff --git a/Main.py b/Main.py index de6b467f93d9..d105bd4ad0e5 100644 --- a/Main.py +++ b/Main.py @@ -11,7 +11,8 @@ import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region -from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items +from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \ + flood_items from Options import StartInventoryPool from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings @@ -45,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) @@ -100,7 +104,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.early_items[player][item_name] = max(0, early-count) remaining_count = count-early if remaining_count > 0: - local_early = multiworld.early_local_items[player].get(item_name, 0) + local_early = multiworld.local_early_items[player].get(item_name, 0) if local_early: multiworld.early_items[player][item_name] = max(0, local_early - remaining_count) del local_early @@ -124,14 +128,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in multiworld.player_ids: exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value) multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value + world_excluded_locations = set() for location_name in multiworld.worlds[player].options.priority_locations.value: try: location = multiworld.get_location(location_name, player) - except KeyError as e: # failed to find the given location. Check if it's a legitimate location - if location_name not in multiworld.worlds[player].location_name_to_id: - raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e - else: + except KeyError: + continue + + if location.progress_type != LocationProgressType.EXCLUDED: location.progress_type = LocationProgressType.PRIORITY + else: + logger.warning(f"Unable to prioritize location \"{location_name}\" in player {player}'s world because the world excluded it.") + world_excluded_locations.add(location_name) + multiworld.worlds[player].options.priority_locations.value -= world_excluded_locations # Set local and non-local item rules. if multiworld.players > 1: @@ -144,117 +153,40 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # remove starting inventory from pool items. # Because some worlds don't actually create items during create_items this has to be as late as possible. - if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids): - new_items: List[Item] = [] - depletion_pool: Dict[int, Dict[str, int]] = { - player: getattr(multiworld.worlds[player].options, - "start_inventory_from_pool", - StartInventoryPool({})).value.copy() - for player in multiworld.player_ids - } - for player, items in depletion_pool.items(): - player_world: AutoWorld.World = multiworld.worlds[player] - for count in items.values(): - for _ in range(count): - new_items.append(player_world.create_filler()) - target: int = sum(sum(items.values()) for items in depletion_pool.values()) - for i, item in enumerate(multiworld.itempool): + fallback_inventory = StartInventoryPool({}) + depletion_pool: Dict[int, Dict[str, int]] = { + player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy() + for player in multiworld.player_ids + } + target_per_player = { + player: sum(target_items.values()) for player, target_items in depletion_pool.items() if target_items + } + + if target_per_player: + new_itempool: List[Item] = [] + + # Make new itempool with start_inventory_from_pool items removed + for item in multiworld.itempool: if depletion_pool[item.player].get(item.name, 0): - target -= 1 depletion_pool[item.player][item.name] -= 1 - # quick abort if we have found all items - if not target: - new_items.extend(multiworld.itempool[i+1:]) - break else: - new_items.append(item) - - # leftovers? - if target: - for player, remaining_items in depletion_pool.items(): - remaining_items = {name: count for name, count in remaining_items.items() if count} - if remaining_items: - raise Exception(f"{multiworld.get_player_name(player)}" - f" is trying to remove items from their pool that don't exist: {remaining_items}") - assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." - multiworld.itempool[:] = new_items - - # temporary home for item links, should be moved out of Main - for group_id, group in multiworld.groups.items(): - def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ - Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] - ]: - classifications: Dict[str, int] = collections.defaultdict(int) - counters = {player: {name: 0 for name in shared_pool} for player in players} - for item in multiworld.itempool: - if item.player in counters and item.name in shared_pool: - counters[item.player][item.name] += 1 - classifications[item.name] |= item.classification - - for player in players.copy(): - if all([counters[player][item] == 0 for item in shared_pool]): - players.remove(player) - del (counters[player]) - - if not players: - return None, None - - for item in shared_pool: - count = min(counters[player][item] for player in players) - if count: - for player in players: - counters[player][item] = count - else: - for player in players: - del (counters[player][item]) - return counters, classifications + new_itempool.append(item) - common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) - if not common_item_count: - continue + # Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool + for player, target in target_per_player.items(): + unfound_items = {item: count for item, count in depletion_pool[player].items() if count} - new_itempool: List[Item] = [] - for item_name, item_count in next(iter(common_item_count.values())).items(): - for _ in range(item_count): - new_item = group["world"].create_item(item_name) - # mangle together all original classification bits - new_item.classification |= classifications[item_name] - new_itempool.append(new_item) - - region = Region("Menu", group_id, multiworld, "ItemLink") - multiworld.regions.append(region) - locations = region.locations - for item in multiworld.itempool: - count = common_item_count.get(item.player, {}).get(item.name, 0) - if count: - loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}", - None, region) - loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ - state.has(item_name, group_id_, count_) - - locations.append(loc) - loc.place_locked_item(item) - common_item_count[item.player][item.name] -= 1 - else: - new_itempool.append(item) + if unfound_items: + player_name = multiworld.get_player_name(player) + logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}") - itemcount = len(multiworld.itempool) - multiworld.itempool = new_itempool + needed_items = target_per_player[player] - sum(unfound_items.values()) + new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)] - while itemcount > len(multiworld.itempool): - items_to_add = [] - for player in group["players"]: - if group["link_replacement"]: - item_player = group_id - else: - item_player = player - if group["replacement_items"][player]: - items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player, - group["replacement_items"][player])) - else: - items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player)) - multiworld.random.shuffle(items_to_add) - multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)]) + assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change." + multiworld.itempool[:] = new_itempool + + multiworld.link_items() if any(multiworld.item_links.values()): multiworld._all_state = None @@ -310,6 +242,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ def write_multidata(): import NetUtils + from NetUtils import HintStatus slot_data = {} client_versions = {} games = {} @@ -334,10 +267,10 @@ def write_multidata(): for slot in multiworld.player_ids: slot_data[slot] = multiworld.worlds[slot].fill_slot_data() - def precollect_hint(location): + def precollect_hint(location: Location, auto_status: HintStatus): entrance = er_hint_data.get(location.player, {}).get(location.address, "") hint = NetUtils.Hint(location.item.player, location.player, location.address, - location.item.code, False, entrance, location.item.flags) + location.item.code, False, entrance, location.item.flags, auto_status) precollected_hints[location.player].add(hint) if location.item.player not in multiworld.groups: precollected_hints[location.item.player].add(hint) @@ -350,19 +283,22 @@ def precollect_hint(location): if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ "location.address should then also be None. Location: " \ - f" {location}" + f" {location}, Item: {location.item}" assert location.address not in locations_data[location.player], ( f"Locations with duplicate address. {location} and " f"{locations_data[location.player][location.address]}") locations_data[location.player][location.address] = \ location.item.code, location.item.player, location.item.flags + auto_status = HintStatus.HINT_AVOID if location.item.trap else HintStatus.HINT_PRIORITY if location.name in multiworld.worlds[location.player].options.start_location_hints: - precollect_hint(location) + if not location.item.trap: # Unspecified status for location hints, except traps + auto_status = HintStatus.HINT_UNSPECIFIED + precollect_hint(location, auto_status) elif location.item.name in multiworld.worlds[location.item.player].options.start_hints: - precollect_hint(location) + precollect_hint(location, auto_status) elif any([location.item.name in multiworld.worlds[player].options.start_hints for player in multiworld.groups.get(location.item.player, {}).get("players", [])]): - precollect_hint(location) + precollect_hint(location, auto_status) # embedded data package data_package = { @@ -374,11 +310,10 @@ def precollect_hint(location): # get spheres -> filter address==None -> skip empty spheres: List[Dict[int, Set[int]]] = [] - for sphere in multiworld.get_spheres(): + for sphere in multiworld.get_sendable_spheres(): current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) for sphere_location in sphere: - if type(sphere_location.address) is int: - current_sphere[sphere_location.player].add(sphere_location.address) + current_sphere[sphere_location.player].add(sphere_location.address) if current_sphere: spheres.append(dict(current_sphere)) @@ -399,6 +334,7 @@ def precollect_hint(location): "seed_name": multiworld.seed_name, "spheres": spheres, "datapackage": data_package, + "race_mode": int(multiworld.is_race), } AutoWorld.call_all(multiworld, "modify_multidata", multidata) @@ -411,7 +347,7 @@ def precollect_hint(location): output_file_futures.append(pool.submit(write_multidata)) if not check_accessibility_task.result(): if not multiworld.can_beat_game(): - raise Exception("Game appears as unbeatable. Aborting.") + raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld) else: logger.warning("Location Accessibility requirements not fulfilled.") diff --git a/ModuleUpdate.py b/ModuleUpdate.py index ed041bef4604..04cf25ea5594 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -5,8 +5,15 @@ import warnings -if sys.version_info < (3, 8, 6): - raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") +if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11): + # Official micro version updates. This should match the number in docs/running from source.md. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.10.15+ is supported.") +elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15): + # There are known security issues, but no easy way to install fixed versions on Windows for testing. + warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.") +elif sys.version_info < (3, 10, 1): + # Other platforms may get security backports instead of micro updates, so the number is unreliable. + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) @@ -75,13 +82,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 dc5e3d21ac89..0601e179152c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -15,6 +15,7 @@ import operator import pickle import random +import shlex import threading import time import typing @@ -40,7 +41,8 @@ import Utils from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ - SlotType, LocationStore + SlotType, LocationStore, Hint, HintStatus +from BaseClasses import ItemClassification min_client_version = Version(0, 1, 6) colorama.init() @@ -67,6 +69,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: @@ -169,11 +186,9 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) + item_names: typing.Dict[str, typing.Dict[int, str]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) + location_names: typing.Dict[str, typing.Dict[int, str]] location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -182,7 +197,6 @@ class Context: """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger - def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -215,7 +229,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.hint_cost = hint_cost self.location_check_points = location_check_points self.hints_used = collections.defaultdict(int) - self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set) + self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set) self.release_mode: str = release_mode self.remaining_mode: str = remaining_mode self.collect_mode: str = collect_mode @@ -253,6 +267,10 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.location_name_groups = {} self.all_item_and_group_names = {} self.all_location_and_group_names = {} + self.item_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')) + self.location_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')) self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() @@ -412,6 +430,8 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A use_embedded_server_options: bool): self.read_data = {} + # there might be a better place to put this. + self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0) mdata_ver = decoded_obj["minimum_versions"]["server"] if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," @@ -551,6 +571,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() @@ -634,13 +657,29 @@ def get_hint_cost(self, slot): return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot]))) return 0 - def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None): + def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None, + changed: typing.Optional[typing.Set[team_slot]] = None) -> None: + """Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot + will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot) + pair that has at least one hint modified will be added to the set. + """ for hint_team, hint_slot in self.hints: - if (team is None or team == hint_team) and (slot is None or slot == hint_slot): - self.hints[hint_team, hint_slot] = { - hint.re_check(self, hint_team) for hint in - self.hints[hint_team, hint_slot] - } + if team != hint_team and team is not None: + continue # Check specified team only, all if team is None + if slot != hint_slot and slot is not None: + continue # Check specified slot only, all if slot is None + new_hints: typing.Set[Hint] = set() + for hint in self.hints[hint_team, hint_slot]: + new_hint = hint.re_check(self, hint_team) + new_hints.add(new_hint) + if hint == new_hint: + continue + for player in self.slot_set(hint.receiving_player) | {hint.finding_player}: + if changed is not None: + changed.add((hint_team,player)) + if slot is not None and slot != player: + self.replace_hint(hint_team, player, hint, new_hint) + self.hints[hint_team, hint_slot] = new_hints def get_rechecked_hints(self, team: int, slot: int): self.recheck_hints(team, slot) @@ -689,7 +728,7 @@ def get_aliased_name(self, team: int, slot: int): else: return self.player_names[team, slot] - def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False, + def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False, recipients: typing.Sequence[int] = None): """Send and remember hints.""" if only_new: @@ -705,15 +744,15 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b if not hint.local and data not in concerns[hint.finding_player]: concerns[hint.finding_player].append(data) # remember hints in all cases - if not hint.found: - # since hints are bidirectional, finding player and receiving player, - # we can check once if hint already exists - if hint not in self.hints[team, hint.finding_player]: - self.hints[team, hint.finding_player].add(hint) - new_hint_events.add(hint.finding_player) - for player in self.slot_set(hint.receiving_player): - self.hints[team, player].add(hint) - new_hint_events.add(player) + + # since hints are bidirectional, finding player and receiving player, + # we can check once if hint already exists + if hint not in self.hints[team, hint.finding_player]: + self.hints[team, hint.finding_player].add(hint) + new_hint_events.add(hint.finding_player) + for player in self.slot_set(hint.receiving_player): + self.hints[team, player].add(hint) + new_hint_events.add(player) self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint))) for slot in new_hint_events: @@ -727,6 +766,17 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b for client in clients: async_start(self.send_msgs(client, client_hints)) + def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]: + for hint in self.hints[team, finding_player]: + if hint.location == seeked_location: + return hint + return None + + def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None: + if old_hint in self.hints[team, slot]: + self.hints[team, slot].remove(old_hint) + self.hints[team, slot].add(new_hint) + # "events" def on_goal_achieved(self, client: Client): @@ -925,9 +975,13 @@ def get_status_string(ctx: Context, team: int, tag: str): tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags]) completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else "" - goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." + status_text = ( + " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else + " and is ready." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_READY else + "." + ) text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ - f"{tag_text}{goal_text} {completion_text}" + f"{tag_text}{status_text} {completion_text}" return text @@ -991,7 +1045,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False): collect_player(ctx, team, group, True) -def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: +def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]: return ctx.locations.get_remaining(ctx.location_checks, team, slot) @@ -1028,14 +1082,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi "hint_points": get_slot_points(ctx, team, slot), "checked_locations": new_locations, # send back new checks only }]) - old_hints = ctx.hints[team, slot].copy() - ctx.recheck_hints(team, slot) - if old_hints != ctx.hints[team, slot]: - ctx.on_changed_hints(team, slot) + updated_slots: typing.Set[tuple[int, int]] = set() + ctx.recheck_hints(team, slot, updated_slots) + for hint_team, hint_slot in updated_slots: + ctx.on_changed_hints(hint_team, hint_slot) ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \ + -> typing.List[Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): @@ -1045,31 +1100,58 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item] for finding_player, location_id, item_id, receiving_player, item_flags \ in ctx.locations.find_item(slots, seeked_item_id): - found = location_id in ctx.location_checks[team, finding_player] - entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") - hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance, - item_flags)) + prev_hint = ctx.get_hint(team, slot, location_id) + if prev_hint: + hints.append(prev_hint) + else: + found = location_id in ctx.location_checks[team, finding_player] + entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance, + item_flags, new_status)) return hints -def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: +def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \ + -> typing.List[Hint]: seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] - return collect_hint_location_id(ctx, team, slot, seeked_location) + return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status) -def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]: +def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \ + -> typing.List[Hint]: + prev_hint = ctx.get_hint(team, slot, seeked_location) + if prev_hint: + return [prev_hint] result = ctx.locations[slot].get(seeked_location, (None, None, None)) if any(result): item_id, receiving_player, item_flags = result found = seeked_location in ctx.location_checks[team, slot] entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") - return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)] + new_status = auto_status + if found: + new_status = HintStatus.HINT_FOUND + elif item_flags & ItemClassification.trap: + new_status = HintStatus.HINT_AVOID + return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags, + new_status)] return [] -def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +def format_hint(ctx: Context, team: int, hint: Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ @@ -1077,7 +1159,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: if hint.entrance: text += f" at {hint.entrance}" - return text + (". (found)" if hint.found else ".") + + return text + ". " + status_names.get(hint.status, "(unknown)") def json_format_send_event(net_item: NetworkItem, receiving_player: int): @@ -1132,7 +1215,10 @@ def __call__(self, raw: str) -> typing.Optional[bool]: if not raw: return try: - command = raw.split() + try: + command = shlex.split(raw, comments=False) + except ValueError: # most likely: "ValueError: No closing quotation" + command = raw.split() basecommand = command[0] if basecommand[0] == self.marker: method = self.commands.get(basecommand[1:].lower(), None) @@ -1203,6 +1289,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 @@ -1350,10 +1440,10 @@ def _cmd_collect(self) -> bool: def _cmd_remaining(self) -> bool: """List remaining items in your game, but not their location or recipient""" if self.ctx.remaining_mode == "enabled": - remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) - if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] - for item_id in remaining_item_ids)) + rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) + if rest_locations: + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] + for slot, item_id in rest_locations)) else: self.output("No remaining items found.") return True @@ -1363,10 +1453,10 @@ def _cmd_remaining(self) -> bool: return False else: # is goal if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: - remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) - if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] - for item_id in remaining_item_ids)) + rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) + if rest_locations: + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] + for slot, item_id in rest_locations)) else: self.output("No remaining items found.") return True @@ -1474,7 +1564,7 @@ def _cmd_getitem(self, item_name: str) -> bool: def get_hints(self, input_text: str, for_location: bool = False) -> bool: points_available = get_client_points(self.ctx, self.client) cost = self.ctx.get_hint_cost(self.client.slot) - + auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY if not input_text: hints = {hint.re_check(self.ctx, self.client.team) for hint in self.ctx.hints[self.client.team, self.client.slot]} @@ -1500,9 +1590,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] elif not for_location: - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: - hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id) + hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status) else: game = self.ctx.games[self.client.slot] @@ -1522,16 +1612,16 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: hints = [] for item_name in self.ctx.item_name_groups[game][hint_name]: if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) + hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status)) elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name - hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) elif hint_name in self.ctx.location_name_groups[game]: # location group name hints = [] for loc_name in self.ctx.location_name_groups[game][hint_name]: if loc_name in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)) + hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status)) else: # location name - hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) + hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status) else: self.output(response) @@ -1803,13 +1893,56 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): target_item, target_player, flags = ctx.locations[client.slot][location] if create_as_hint: - hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location)) + hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location, + HintStatus.HINT_UNSPECIFIED)) locs.append(NetworkItem(target_item, location, target_player, flags)) ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2) if locs and create_as_hint: ctx.save() await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) - + + elif cmd == 'UpdateHint': + location = args["location"] + player = args["player"] + status = args["status"] + if not isinstance(player, int) or not isinstance(location, int) \ + or (status is not None and not isinstance(status, int)): + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint', + "original_cmd": cmd}]) + return + hint = ctx.get_hint(client.team, player, location) + if not hint: + return # Ignored safely + if client.slot not in ctx.slot_set(hint.receiving_player): + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission', + "original_cmd": cmd}]) + return + new_hint = hint + if status is None: + return + try: + status = HintStatus(status) + except ValueError: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Invalid Status', "original_cmd": cmd}]) + return + if status == HintStatus.HINT_FOUND: + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", + "text": 'UpdateHint: Cannot manually update status to "HINT_FOUND"', "original_cmd": cmd}]) + return + new_hint = new_hint.re_prioritize(ctx, status) + if hint == new_hint: + return + ctx.replace_hint(client.team, hint.finding_player, hint, new_hint) + ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint) + ctx.save() + ctx.on_changed_hints(client.team, hint.finding_player) + ctx.on_changed_hints(client.team, hint.receiving_player) + elif cmd == 'StatusUpdate': update_client_status(ctx, client, args["status"]) @@ -1931,8 +2064,10 @@ def _cmd_status(self, tag: str = "") -> bool: def _cmd_exit(self) -> bool: """Shutdown the server""" - self.ctx.server.ws_server.close() - self.ctx.exit_event.set() + try: + self.ctx.server.ws_server.close() + finally: + self.ctx.exit_event.set() return True @mark_raw @@ -2039,6 +2174,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) @@ -2110,9 +2247,9 @@ def _cmd_hint(self, player_name: str, *item_name: str) -> bool: hints = [] for item_name_from_group in self.ctx.item_name_groups[game][item]: if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID - hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) + hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY)) else: # item name or id - hints = collect_hints(self.ctx, team, slot, item) + hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY) if hints: self.ctx.notify_hints(team, hints) @@ -2146,14 +2283,17 @@ def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: if usable: if isinstance(location, int): - hints = collect_hint_location_id(self.ctx, team, slot, location) + hints = collect_hint_location_id(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]: hints = [] for loc_name_from_group in self.ctx.location_name_groups[game][location]: if loc_name_from_group in self.ctx.location_names_for_game(game): - hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group)) + hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group, + HintStatus.HINT_UNSPECIFIED)) else: - hints = collect_hint_location_name(self.ctx, team, slot, location) + hints = collect_hint_location_name(self.ctx, team, slot, location, + HintStatus.HINT_UNSPECIFIED) if hints: self.ctx.notify_hints(team, hints) else: @@ -2243,6 +2383,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument('--cert_key', help="Path to SSL Certificate Key file") parser.add_argument('--loglevel', default=defaults["loglevel"], choices=['debug', 'info', 'warning', 'error', 'critical']) + parser.add_argument('--logtime', help="Add timestamps to STDOUT", + default=defaults["logtime"], action='store_true') parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int) parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int) parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true') @@ -2323,7 +2465,9 @@ def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLConte async def main(args: argparse.Namespace): - Utils.init_logging("Server", loglevel=args.loglevel.lower()) + Utils.init_logging(name="Server", + loglevel=args.loglevel.lower(), + add_timestamp=args.logtime) ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode, diff --git a/NetUtils.py b/NetUtils.py index f8d698c74fcc..a961850639a0 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -29,6 +29,14 @@ class ClientStatus(ByValue, enum.IntEnum): CLIENT_GOAL = 30 +class HintStatus(enum.IntEnum): + HINT_FOUND = 0 + HINT_UNSPECIFIED = 1 + HINT_NO_PRIORITY = 10 + HINT_AVOID = 20 + HINT_PRIORITY = 30 + + class SlotType(ByValue, enum.IntFlag): spectator = 0b00 player = 0b01 @@ -79,6 +87,7 @@ class NetworkItem(typing.NamedTuple): item: int location: int player: int + """ Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """ flags: int = 0 @@ -223,7 +232,7 @@ def _handle_text(self, node: JSONMessagePart): def _handle_player_id(self, node: JSONMessagePart): player = int(node["text"]) - node["color"] = 'magenta' if player == self.ctx.slot else 'yellow' + node["color"] = 'magenta' if self.ctx.slot_concerns_self(player) else 'yellow' node["text"] = self.ctx.player_names[player] return self._handle_color(node) @@ -272,7 +281,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): @@ -295,6 +305,20 @@ def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs}) +status_names: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "(found)", + HintStatus.HINT_UNSPECIFIED: "(unspecified)", + HintStatus.HINT_NO_PRIORITY: "(no priority)", + HintStatus.HINT_AVOID: "(avoid)", + HintStatus.HINT_PRIORITY: "(priority)", +} +status_colors: typing.Dict[HintStatus, str] = { + HintStatus.HINT_FOUND: "green", + HintStatus.HINT_UNSPECIFIED: "white", + HintStatus.HINT_NO_PRIORITY: "slateblue", + HintStatus.HINT_AVOID: "salmon", + HintStatus.HINT_PRIORITY: "plum", +} class Hint(typing.NamedTuple): receiving_player: int finding_player: int @@ -303,14 +327,21 @@ class Hint(typing.NamedTuple): found: bool entrance: str = "" item_flags: int = 0 + status: HintStatus = HintStatus.HINT_UNSPECIFIED def re_check(self, ctx, team) -> Hint: - if self.found: + if self.found and self.status == HintStatus.HINT_FOUND: return self found = self.location in ctx.location_checks[team, self.finding_player] if found: - return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance, - self.item_flags) + return self._replace(found=found, status=HintStatus.HINT_FOUND) + return self + + def re_prioritize(self, ctx, status: HintStatus) -> Hint: + if self.found and status != HintStatus.HINT_FOUND: + status = HintStatus.HINT_FOUND + if status != self.status: + return self._replace(status=status) return self def __hash__(self): @@ -332,10 +363,8 @@ def as_network_message(self) -> dict: else: add_json_text(parts, "'s World") add_json_text(parts, ". ") - if self.found: - add_json_text(parts, "(found)", type="color", color="green") - else: - add_json_text(parts, "(not found)", type="color", color="red") + add_json_text(parts, status_names.get(self.status, "(unknown)"), type="color", + color=status_colors.get(self.status, "red")) return {"cmd": "PrintJSON", "data": parts, "type": "Hint", "receiving": self.receiving_player, @@ -381,6 +410,8 @@ def get_checked(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int] checked = state[team, slot] if not checked: # This optimizes the case where everyone connects to a fresh game at the same time. + if slot not in self: + raise KeyError(slot) return [] return [location_id for location_id in self[slot] if @@ -397,12 +428,12 @@ def get_missing(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int] location_id not in checked] def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int - ) -> typing.List[int]: + ) -> typing.List[typing.Tuple[int, int]]: checked = state[team, slot] player_locations = self[slot] - return sorted([player_locations[location_id][0] for - location_id in player_locations if - location_id not in checked]) + return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for + location_id in player_locations if + location_id not in checked]) if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub diff --git a/Options.py b/Options.py index b5fb25ea34a0..4e26a0d56c5c 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_file_safe_name, 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 @@ -753,7 +754,7 @@ def __init__(self, value: int) -> None: elif value > self.range_end and value not in self.special_range_names.values(): raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " + f"and is also not one of the supported named special values: {self.special_range_names}") - + # See docstring for key in self.special_range_names: if key != key.lower(): @@ -786,17 +787,22 @@ class VerifyKeys(metaclass=FreezeValidKeys): verify_location_name: bool = False value: typing.Any - @classmethod - def verify_keys(cls, data: typing.Iterable[str]) -> None: - if cls.valid_keys: - data = set(data) - dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) - extra = dataset - cls._valid_keys + def verify_keys(self) -> None: + if self.valid_keys: + data = set(self.value) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys if extra: - raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " - f"Allowed keys: {cls._valid_keys}.") + raise OptionError( + f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed keys: {self._valid_keys}." + ) def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + try: + self.verify_keys() + except OptionError as validation_error: + raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}") if self.convert_name_groups and self.verify_item_name: new_value = type(self.value)() # empty container of whatever value is for item_name in self.value: @@ -822,7 +828,10 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P f"is not a valid location name from {world.game}. " f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") + def __iter__(self) -> typing.Iterator[typing.Any]: + return self.value.__iter__() + class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): default = {} supports_weighting = False @@ -833,7 +842,6 @@ def __init__(self, value: typing.Dict[str, typing.Any]): @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: if type(data) == dict: - cls.verify_keys(data) return cls(data) else: raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") @@ -855,6 +863,8 @@ class ItemDict(OptionDict): verify_item_name = True def __init__(self, value: typing.Dict[str, int]): + if any(item_count is None for item_count in value.values()): + raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .") if any(item_count < 1 for item_count in value.values()): raise Exception("Cannot have non-positive item counts.") super(ItemDict, self).__init__(value) @@ -879,7 +889,6 @@ def from_text(cls, text: str): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -905,7 +914,6 @@ def from_text(cls, text: str): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -948,6 +956,19 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P self.value = [] logging.warning(f"The plando texts module is turned off, " f"so text for {player_name} will be ignored.") + else: + super().verify(world, player_name, plando_options) + + def verify_keys(self) -> None: + if self.valid_keys: + data = set(text.at for text in self) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys + if extra: + raise OptionError( + f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed placements: {self._valid_keys}." + ) @classmethod def from_any(cls, data: PlandoTextsFromAnyType) -> Self: @@ -958,7 +979,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( @@ -966,12 +999,13 @@ 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) else: raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") - cls.verify_keys([text.at for text in texts]) return cls(texts) else: raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") @@ -1144,18 +1178,35 @@ def __len__(self) -> int: class Accessibility(Choice): - """Set rules for reachability of your items/locations. + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and acquired. - - **Locations:** ensure everything can be reached and acquired. - - **Items:** ensure all logically relevant items can be acquired. - - **Minimal:** ensure what is needed to reach your goal can be acquired. + **Minimal:** ensure what is needed to reach your goal can be acquired. """ display_name = "Accessibility" rich_text_doc = True - option_locations = 0 - option_items = 1 + option_full = 0 option_minimal = 2 alias_none = 2 + alias_locations = 0 + alias_items = 0 + default = 0 + + +class ItemsAccessibility(Accessibility): + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and acquired. + + **Minimal:** ensure what is needed to reach your goal can be acquired. + + **Items:** ensure all logically relevant items can be acquired. Some items, such as keys, may be self-locking, and + some locations may be inaccessible. + """ + option_items = 1 default = 1 @@ -1198,13 +1249,18 @@ class CommonOptions(metaclass=OptionsMetaProperty): progression_balancing: ProgressionBalancing accessibility: Accessibility - def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]: + def as_dict(self, + *option_names: str, + casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake", + toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]: """ Returns a dictionary of [str, Option.value] :param option_names: names of the options to return :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` + :param toggles_as_bools: whether toggle options should be output as bools instead of strings """ + assert option_names, "options.as_dict() was used without any option names." option_results = {} for option_name in option_names: if option_name in type(self).type_hints: @@ -1224,6 +1280,8 @@ def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, value = getattr(self, option_name).value if isinstance(value, set): value = sorted(value) + elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle): + value = bool(value) option_results[display_name] = value else: raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") @@ -1289,7 +1347,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 @@ -1413,22 +1471,26 @@ class OptionGroup(typing.NamedTuple): def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[ str, typing.Dict[str, typing.Type[Option[typing.Any]]]]: """Generates and returns a dictionary for the option groups of a specified world.""" - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - # add a default option group for uncategorized options to get thrown into - ordered_groups = ["Game Options"] - [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - if visibility_level & option.visibility: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option + option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()} - # if the world doesn't have any ungrouped options, this group will be empty so just remove it - if not grouped_options["Game Options"]: - del grouped_options["Game Options"] + ordered_groups = {group.name: group.options for group in world.web.option_groups} - return grouped_options + # add a default option group for uncategorized options to get thrown into + if "Game Options" not in ordered_groups: + grouped_options = set(option for group in ordered_groups.values() for option in group) + ungrouped_options = [option for option in option_to_name if option not in grouped_options] + # only add the game options group if we have ungrouped options + if ungrouped_options: + ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups} + + return { + group: { + option_to_name[option]: option + for option in group_options + if (visibility_level in option.visibility and option in option_to_name) + } + for group, group_options in ordered_groups.items() + } def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: @@ -1484,33 +1546,44 @@ def yaml_dump_scalar(scalar) -> str: del file_data - with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: + with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) -if __name__ == "__main__": - - from worlds.alttp.Options import Logic - import argparse - - map_shuffle = Toggle - compass_shuffle = Toggle - key_shuffle = Toggle - big_key_shuffle = Toggle - hints = Toggle - test = argparse.Namespace() - test.logic = Logic.from_text("no_logic") - test.map_shuffle = map_shuffle.from_text("ON") - test.hints = hints.from_text('OFF') - try: - test.logic = Logic.from_text("overworld_glitches_typo") - except KeyError as e: - print(e) - try: - test.logic_owg = Logic.from_text("owg") - except KeyError as e: - print(e) - if test.map_shuffle: - print("map_shuffle is on") - print(f"Hints are {bool(test.hints)}") - print(test) +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/README.md b/README.md index cebd4f7e7529..36b7a07fb4b3 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,13 @@ Currently, the following games are supported: * Aquaria * Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 * A Hat in Time +* Old School Runescape +* Kingdom Hearts 1 +* Mega Man 2 +* Yacht Dice +* Faxanadu +* Saving Princess +* Castlevania: Circle of the Moon For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/SNIClient.py b/SNIClient.py index 222ed54f5cc5..19440e1dc5be 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -633,7 +633,13 @@ async def game_watcher(ctx: SNIContext) -> None: if not ctx.client_handler: continue - rom_validated = await ctx.client_handler.validate_rom(ctx) + try: + rom_validated = await ctx.client_handler.validate_rom(ctx) + except Exception as e: + snes_logger.error(f"An error occurred, see logs for details: {e}") + text_file_logger = logging.getLogger() + text_file_logger.exception(e) + rom_validated = False if not rom_validated or (ctx.auth and ctx.auth != ctx.rom): snes_logger.warning("ROM change detected, please reconnect to the multiworld server") @@ -649,7 +655,13 @@ async def game_watcher(ctx: SNIContext) -> None: perf_counter = time.perf_counter() - await ctx.client_handler.game_watcher(ctx) + try: + await ctx.client_handler.game_watcher(ctx) + except Exception as e: + snes_logger.error(f"An error occurred, see logs for details: {e}") + text_file_logger = logging.getLogger() + text_file_logger.exception(e) + await snes_disconnect(ctx) async def run_game(romfile: str) -> None: diff --git a/UndertaleClient.py b/UndertaleClient.py index 415d7e7f21a3..dfacee148abc 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -29,7 +29,7 @@ def _cmd_resync(self): def _cmd_patch(self): """Patch the game. Only use this command if /auto_patch fails.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) self.ctx.patch_game() self.output("Patched.") @@ -43,7 +43,7 @@ def _cmd_savepath(self, directory: str): def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): """Patch the game automatically.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) tempInstall = steaminstall if not os.path.isfile(os.path.join(tempInstall, "data.win")): tempInstall = None @@ -62,7 +62,7 @@ def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): for file_name in os.listdir(tempInstall): if file_name != "steam_api.dll": shutil.copy(os.path.join(tempInstall, file_name), - os.path.join(os.getcwd(), "Undertale", file_name)) + Utils.user_path("Undertale", file_name)) self.ctx.patch_game() self.output("Patching successful!") @@ -111,12 +111,12 @@ def __init__(self, server_address, password): self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE") def patch_game(self): - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f: + with open(Utils.user_path("Undertale", "data.win"), "rb") as f: patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff")) - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f: + with open(Utils.user_path("Undertale", "data.win"), "wb") as f: f.write(patchedFile) - os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True) - with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites", + os.makedirs(name=Utils.user_path("Undertale", "Custom Sprites"), exist_ok=True) + with open(os.path.expandvars(Utils.user_path("Undertale", "Custom Sprites", "Which Character.txt")), "w") as f: f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only " "line other than this one.\n", "frisk"]) diff --git a/Utils.py b/Utils.py index f89330cf7c65..574c006b503d 100644 --- a/Utils.py +++ b/Utils.py @@ -18,8 +18,8 @@ from argparse import Namespace from settings import Settings, get_settings -from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union -from typing_extensions import TypeGuard +from time import sleep +from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard from yaml import load, load_all, dump try: @@ -31,6 +31,7 @@ import tkinter import pathlib from BaseClasses import Region + import multiprocessing def tuplize_version(version: str) -> Version: @@ -46,7 +47,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.5.0" +__version__ = "0.6.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -420,10 +421,11 @@ def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # used by MultiServer -> savegame/multidata - if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: + if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", + "SlotType", "NetworkSlot", "HintStatus"}: return getattr(self.net_utils_module, name) # Options and Plando are unpickled by WebHost -> Generate - if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: + if module == "worlds.generic" and name == "PlandoItem": if not self.generic_properties_module: self.generic_properties_module = importlib.import_module("worlds.generic") return getattr(self.generic_properties_module, name) @@ -434,7 +436,7 @@ def find_class(self, module: str, name: str) -> type: else: mod = importlib.import_module(module) obj = getattr(mod, name) - if issubclass(obj, self.options_module.Option): + if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)): return obj # Forbid everything else. raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") @@ -483,9 +485,9 @@ def get_text_after(text: str, start: str) -> str: loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG} -def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w", - log_format: str = "[%(name)s at %(asctime)s]: %(message)s", - exception_logger: typing.Optional[str] = None): +def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, + write_mode: str = "w", log_format: str = "[%(name)s at %(asctime)s]: %(message)s", + add_timestamp: bool = False, exception_logger: typing.Optional[str] = None): import datetime loglevel: int = loglevel_mapping.get(loglevel, loglevel) log_folder = user_path("logs") @@ -513,10 +515,14 @@ def filter(self, record: logging.LogRecord) -> bool: return self.condition(record) file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False))) + file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg)) root_logger.addHandler(file_handler) if sys.stdout: + formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') stream_handler = logging.StreamHandler(sys.stdout) stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False))) + if add_timestamp: + stream_handler.setFormatter(formatter) root_logger.addHandler(stream_handler) # Relay unhandled exceptions to logger. @@ -528,7 +534,8 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.__excepthook__(exc_type, exc_value, exc_traceback) return logging.getLogger(exception_logger).exception("Uncaught exception", - exc_info=(exc_type, exc_value, exc_traceback)) + exc_info=(exc_type, exc_value, exc_traceback), + extra={"NoStream": exception_logger is None}) return orig_hook(exc_type, exc_value, exc_traceback) handle_exception._wrapped = True @@ -551,7 +558,7 @@ def _cleanup(): import platform logging.info( f"Archipelago ({__version__}) logging initialized" - f" on {platform.platform()}" + f" on {platform.platform()} process {os.getpid()}" f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" f"{' (frozen)' if is_frozen() else ''}" ) @@ -567,6 +574,8 @@ def queuer(): else: if text: queue.put_nowait(text) + else: + sleep(0.01) # non-blocking stream from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) @@ -664,6 +673,19 @@ def get_input_text_from_response(text: str, command: str) -> typing.Optional[str return None +def is_kivy_running() -> bool: + if "kivy" in sys.modules: + from kivy.app import App + return App.get_running_app() is not None + return False + + +def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: + if is_kivy_running(): + raise RuntimeError("kivy should not be running in multiprocess") + res.put(open_filename(*args)) + + def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ -> typing.Optional[str]: logging.info(f"Opening file input dialog for {title}.") @@ -693,6 +715,13 @@ def run(*args: str): f'This attempt was made because open_filename was used for "{title}".') raise e else: + if is_macos and is_kivy_running(): + # on macOS, mixing kivy and tk does not work, so spawn a new process + # FIXME: performance of this is pretty bad, and we should (also) look into alternatives + from multiprocessing import Process, Queue + res: "Queue[typing.Optional[str]]" = Queue() + Process(target=_mp_open_filename, args=(res, title, filetypes, suggest)).start() + return res.get() try: root = tkinter.Tk() except tkinter.TclError: @@ -702,6 +731,12 @@ def run(*args: str): initialfile=suggest or None) +def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: + if is_kivy_running(): + raise RuntimeError("kivy should not be running in multiprocess") + res.put(open_directory(*args)) + + def open_directory(title: str, suggest: str = "") -> typing.Optional[str]: def run(*args: str): return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None @@ -725,9 +760,16 @@ def run(*args: str): import tkinter.filedialog except Exception as e: logging.error('Could not load tkinter, which is likely not installed. ' - f'This attempt was made because open_filename was used for "{title}".') + f'This attempt was made because open_directory was used for "{title}".') raise e else: + if is_macos and is_kivy_running(): + # on macOS, mixing kivy and tk does not work, so spawn a new process + # FIXME: performance of this is pretty bad, and we should (also) look into alternatives + from multiprocessing import Process, Queue + res: "Queue[typing.Optional[str]]" = Queue() + Process(target=_mp_open_directory, args=(res, title, suggest)).start() + return res.get() try: root = tkinter.Tk() except tkinter.TclError: @@ -740,12 +782,6 @@ def messagebox(title: str, text: str, error: bool = False) -> None: def run(*args: str): return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None - def is_kivy_running(): - if "kivy" in sys.modules: - from kivy.app import App - return App.get_running_app() is not None - return False - if is_kivy_running(): from kvui import MessageBox MessageBox(title, text, error).open() @@ -824,11 +860,10 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non task.add_done_callback(_faf_tasks.discard) -def deprecate(message: str): +def deprecate(message: str, add_stacklevels: int = 0): if __debug__: raise Exception(message) - import warnings - warnings.warn(message) + warnings.warn(message, stacklevel=2 + add_stacklevels) class DeprecateDict(dict): @@ -842,10 +877,9 @@ def __init__(self, message: str, error: bool = False) -> None: def __getitem__(self, item: Any) -> Any: if self.should_error: - deprecate(self.log_message) + deprecate(self.log_message, add_stacklevels=1) elif __debug__: - import warnings - warnings.warn(self.log_message) + warnings.warn(self.log_message, stacklevel=2) return super().__getitem__(item) 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..768eeb512289 100644 --- a/WebHost.py +++ b/WebHost.py @@ -1,3 +1,4 @@ +import argparse import os import multiprocessing import logging @@ -11,11 +12,12 @@ # in case app gets imported by something like gunicorn import Utils import settings +from Utils import get_file_safe_name if typing.TYPE_CHECKING: from flask import Flask -Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 +Utils.local_path.cached_path = os.path.dirname(__file__) settings.no_gui = True configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home @@ -31,6 +33,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(allow_abbrev=False) + 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() @@ -61,7 +72,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] shutil.rmtree(base_target_path, ignore_errors=True) for game, world in worlds.items(): # copy files from world's docs folder to the generated folder - target_path = os.path.join(base_target_path, game) + target_path = os.path.join(base_target_path, get_file_safe_name(game)) os.makedirs(target_path, exist_ok=True) if world.zip_path: diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index fdf3037fe015..9c713419c986 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -9,7 +9,7 @@ from pony.flask import Pony from werkzeug.routing import BaseConverter -from Utils import title_sorted +from Utils import title_sorted, get_file_safe_name UPLOAD_FOLDER = os.path.relpath('uploads') LOGS_FOLDER = os.path.relpath('logs') @@ -20,6 +20,7 @@ app.jinja_env.filters['any'] = any app.jinja_env.filters['all'] = all +app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name app.config["SELFHOST"] = True # application process is in charge of running the websites app.config["GENERATORS"] = 8 # maximum concurrent world gens @@ -38,6 +39,8 @@ app.config["JOB_THRESHOLD"] = 1 # after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable. app.config["JOB_TIME"] = 600 +# memory limit for generator processes in bytes +app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296 app.config['SESSION_PERMANENT'] = True # waitress uses one thread for I/O, these are for processing of views that then get sent @@ -84,6 +87,6 @@ def register(): from WebHostLib.customserver import run_server_process # to trigger app routing picking up on it - from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options, session app.register_blueprint(api.api_endpoints) 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/autolauncher.py b/WebHostLib/autolauncher.py index 08a1309ebc73..8ba093e014c5 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -6,6 +6,7 @@ import typing from datetime import timedelta, datetime from threading import Event, Thread +from typing import Any from uuid import UUID from pony.orm import db_session, select, commit @@ -53,7 +54,21 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): generation.state = STATE_STARTED -def init_db(pony_config: dict): +def init_generator(config: dict[str, Any]) -> None: + try: + import resource + except ModuleNotFoundError: + pass # unix only module + else: + # set soft limit for memory to from config (default 4GiB) + soft_limit = config["GENERATOR_MEMORY_LIMIT"] + old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS) + if soft_limit != old_limit: + resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit)) + logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}") + del resource, soft_limit, hard_limit + + pony_config = config["PONY"] db.bind(**pony_config) db.generate_mapping() @@ -105,8 +120,8 @@ def keep_running(): try: with Locker("autogen"): - with multiprocessing.Pool(config["GENERATORS"], initializer=init_db, - initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool: + with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator, + initargs=(config,), maxtasksperchild=10) as generator_pool: with db_session: to_start = select(generation for generation in Generation if generation.state == STATE_STARTED) diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 97cb797f7a56..4e0cf1178f4b 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -105,8 +105,9 @@ def roll_options(options: Dict[str, Union[dict, str]], plando_options=plando_options) else: for i, yaml_data in enumerate(yaml_datas): - rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, - plando_options=plando_options) + if yaml_data is not None: + rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, + plando_options=plando_options) except Exception as e: if e.__cause__: results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}" diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9f70165b61e5..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,10 +335,12 @@ def _done(self, task: asyncio.Future): def run(self): while 1: next_room = rooms_to_run.get(block=True, timeout=None) + gc.collect() task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) task.add_done_callback(self._done) logging.info(f"Starting room {next_room} on {name}.") + del task # delete reference to task object starter = Starter() starter.daemon = True diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index a12dc0f4ae14..0bd9f7e5e066 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -31,11 +31,11 @@ def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[s server_options = { "hint_cost": int(options_source.get("hint_cost", ServerOptions.hint_cost)), - "release_mode": options_source.get("release_mode", ServerOptions.release_mode), - "remaining_mode": options_source.get("remaining_mode", ServerOptions.remaining_mode), - "collect_mode": options_source.get("collect_mode", ServerOptions.collect_mode), + "release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)), + "remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)), + "collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)), "item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))), - "server_password": options_source.get("server_password", None), + "server_password": str(options_source.get("server_password", None)), } generator_options = { "spoiler": int(options_source.get("spoiler", GeneratorOptions.spoiler)), @@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]) elif len(gen_options) > app.config["MAX_ROLL"]: flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") + return redirect(url_for(request.endpoint, **(request.view_args or {}))) elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), @@ -134,6 +135,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 5072f113bd54..6be0e470b3b4 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -1,10 +1,11 @@ import datetime import os -from typing import List, Dict, Union +from typing import Any, IO, Dict, Iterator, List, Tuple, Union import jinja2.exceptions from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory from pony.orm import count, commit, db_session +from werkzeug.utils import secure_filename from worlds.AutoWorld import AutoWorldRegister from . import app, cache @@ -17,13 +18,6 @@ def get_world_theme(game_name: str): return 'grass' -@app.before_request -def register_session(): - session.permanent = True # technically 31 days after the last visit - if not session.get("_id", None): - session["_id"] = uuid4() # uniquely identify each session without needing a login - - @app.errorhandler(404) @app.errorhandler(jinja2.exceptions.TemplateNotFound) def page_not_found(err): @@ -69,14 +63,40 @@ def tutorial_landing(): @app.route('/faq//') @cache.cached() -def faq(lang): - return render_template("faq.html", lang=lang) +def faq(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Frequently Asked Questions", + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), + ) @app.route('/glossary//') @cache.cached() -def terms(lang): - return render_template("glossary.html", lang=lang) +def glossary(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Glossary", + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), + ) @app.route('/seed/') @@ -97,49 +117,91 @@ def new_room(seed: UUID): return redirect(url_for("host_room", room=room.id)) -def _read_log(path: str): - if os.path.exists(path): - with open(path, encoding="utf-8-sig") as log: - yield from log - else: - yield f"Logfile {path} does not exist. " \ - f"Likely a crash during spinup of multiworld instance or it is still spinning up." +def _read_log(log: IO[Any], offset: int = 0) -> Iterator[bytes]: + marker = log.read(3) # skip optional BOM + if marker != b'\xEF\xBB\xBF': + log.seek(0, os.SEEK_SET) + log.seek(offset, os.SEEK_CUR) + yield from log + log.close() # free file handle as soon as possible @app.route('/log/') -def display_log(room: UUID): +def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]: room = Room.get(id=room) if room is None: return abort(404) if room.owner == session["_id"]: file_path = os.path.join("logs", str(room.id) + ".txt") - if os.path.exists(file_path): - return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8") - return "Log File does not exist." + try: + log = open(file_path, "rb") + range_header = request.headers.get("Range") + if range_header: + range_type, range_values = range_header.split('=') + start, end = map(str.strip, range_values.split('-', 1)) + if range_type != "bytes" or end != "": + return "Unsupported range", 500 + # NOTE: we skip Content-Range in the response here, which isn't great but works for our JS + return Response(_read_log(log, int(start)), mimetype="text/plain", status=206) + return Response(_read_log(log), mimetype="text/plain") + except FileNotFoundError: + return Response(f"Logfile {file_path} does not exist. " + f"Likely a crash during spinup of multiworld instance or it is still spinning up.", + mimetype="text/plain") 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 - return render_template("hostRoom.html", room=room, should_refresh=should_refresh) + 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 + fragments: List[str] = [] + for block in _read_log(log): + if raw_size + len(block) > max_size: + fragments.append("â€Ļ") + break + raw_size += len(block) + fragments.append(block.decode("utf-8")) + return "".join(fragments) + except FileNotFoundError: + return "" + + return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log) @app.route('/favicon.ico') diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 33339daa1983..15b7bd61ceee 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -231,6 +231,13 @@ def generate_yaml(game: str): del options[key] + # Detect keys which end with -range, indicating a NamedRange with a possible custom value + elif key_parts[-1].endswith("-range"): + if options[key_parts[-1][:-6]] == "custom": + options[key_parts[-1][:-6]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 3452c9d416db..b7b14dea1e6f 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.6 +pony>=0.7.19 waitress>=3.0.0 Flask-Caching>=2.3.0 Flask-Compress>=1.15 -Flask-Limiter>=3.7.0 -bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.4.1; python_version >= '3.9' +Flask-Limiter>=3.8.0 +bokeh>=3.5.2 markupsafe>=2.1.5 +Markdown>=3.7 +mdx-breakless-lists>=1.0.1 diff --git a/WebHostLib/session.py b/WebHostLib/session.py new file mode 100644 index 000000000000..d5dab7d6e6e6 --- /dev/null +++ b/WebHostLib/session.py @@ -0,0 +1,31 @@ +from uuid import uuid4, UUID + +from flask import session, render_template + +from WebHostLib import app + + +@app.before_request +def register_session(): + session.permanent = True # technically 31 days after the last visit + if not session.get("_id", None): + session["_id"] = uuid4() # uniquely identify each session without needing a login + + +@app.route('/session') +def show_session(): + return render_template( + "session.html", + ) + + +@app.route('/session/') +def set_session(_id: str): + new_id: UUID = UUID(_id, version=4) + old_id: UUID = session["_id"] + if old_id != new_id: + session["_id"] = new_id + return render_template( + "session.html", + old_id=old_id, + ) diff --git a/WebHostLib/static/assets/faq.js b/WebHostLib/static/assets/faq.js deleted file mode 100644 index 1bf5e5a65995..000000000000 --- a/WebHostLib/static/assets/faq.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('faq-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the tutorial is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the tutorial."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/en.md similarity index 98% rename from WebHostLib/static/assets/faq/faq_en.md rename to WebHostLib/static/assets/faq/en.md index fb1ccd2d6f4a..e64535b42d03 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/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/assets/glossary.js b/WebHostLib/static/assets/glossary.js deleted file mode 100644 index 04a292008655..000000000000 --- a/WebHostLib/static/assets/glossary.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('glossary-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the glossary page is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the glossary."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/glossary_en.md b/WebHostLib/static/assets/glossary/en.md similarity index 100% rename from WebHostLib/static/assets/faq/glossary_en.md rename to WebHostLib/static/assets/glossary/en.md diff --git a/WebHostLib/static/assets/playerOptions.js b/WebHostLib/static/assets/playerOptions.js index d0f2e388c2a6..fbf96a3a71c2 100644 --- a/WebHostLib/static/assets/playerOptions.js +++ b/WebHostLib/static/assets/playerOptions.js @@ -288,6 +288,11 @@ const applyPresets = (presetName) => { } }); namedRangeSelect.value = trueValue; + // It is also possible for a preset to use an unnamed value. If this happens, set the dropdown to "Custom" + if (namedRangeSelect.selectedIndex == -1) + { + namedRangeSelect.value = "custom"; + } } // Handle options whose presets are "random" diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png new file mode 100644 index 000000000000..537e27979180 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp new file mode 100644 index 000000000000..f34cd5ff2ec1 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png index 326670b7ebc4..a0b41b0f8cac 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp new file mode 100644 index 000000000000..4a5f2d75a0d4 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png index c8297d34578c..6e1608d82b7f 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp new file mode 100644 index 000000000000..30bd2d047a76 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png index 2a28958e0931..3d3e089ef79f 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp new file mode 100644 index 000000000000..f575ac5d9d48 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png index 9bc84ff603ec..08730d98489c 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp new file mode 100644 index 000000000000..f9227e8f2286 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png index a1e9c7c8b6f5..0bc82fa70e9b 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp new file mode 100644 index 000000000000..3c0a57740263 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png index a40bca60f080..05e675d6a97c 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp new file mode 100644 index 000000000000..4283cd42b16a Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png index b8a8c6a7265e..e0683a74bba5 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp new file mode 100644 index 000000000000..3075cec96add Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png index bb6ccec3d583..cded7ad108d3 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp new file mode 100644 index 000000000000..781b8e4df0d0 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png index dba338f58552..1015819bc8f6 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp new file mode 100644 index 000000000000..73e249f6e530 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png index 33f09b19ce86..7b479bfe7b0b 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp new file mode 100644 index 000000000000..e4ac19bef687 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png index f665015b0d01..59844e31ac42 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp new file mode 100644 index 000000000000..36abe6e552a3 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp differ diff --git a/WebHostLib/static/static/backgrounds/dirt.png b/WebHostLib/static/static/backgrounds/dirt.png index 4ac930edc698..db6bc34635e3 100644 Binary files a/WebHostLib/static/static/backgrounds/dirt.png and b/WebHostLib/static/static/backgrounds/dirt.png differ diff --git a/WebHostLib/static/static/backgrounds/dirt.webp b/WebHostLib/static/static/backgrounds/dirt.webp new file mode 100644 index 000000000000..5a8635506f9f Binary files /dev/null and b/WebHostLib/static/static/backgrounds/dirt.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0001.png b/WebHostLib/static/static/backgrounds/footer/footer-0001.png index b863a3d42952..6752ab4e3279 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0001.png and b/WebHostLib/static/static/backgrounds/footer/footer-0001.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0001.webp b/WebHostLib/static/static/backgrounds/footer/footer-0001.webp new file mode 100644 index 000000000000..fb278c3b1643 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0001.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0002.png b/WebHostLib/static/static/backgrounds/footer/footer-0002.png index 90fdfe95d015..3bacab4134e2 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0002.png and b/WebHostLib/static/static/backgrounds/footer/footer-0002.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0002.webp b/WebHostLib/static/static/backgrounds/footer/footer-0002.webp new file mode 100644 index 000000000000..9b8e457c52a9 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0002.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0003.png b/WebHostLib/static/static/backgrounds/footer/footer-0003.png index 5fc31d1ee970..f8223e690171 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0003.png and b/WebHostLib/static/static/backgrounds/footer/footer-0003.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0003.webp b/WebHostLib/static/static/backgrounds/footer/footer-0003.webp new file mode 100644 index 000000000000..c2ded77536d6 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0003.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0004.png b/WebHostLib/static/static/backgrounds/footer/footer-0004.png index 4a95ce9a3aaf..d4476e53f759 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0004.png and b/WebHostLib/static/static/backgrounds/footer/footer-0004.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0004.webp b/WebHostLib/static/static/backgrounds/footer/footer-0004.webp new file mode 100644 index 000000000000..a2100817461a Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0004.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0005.png b/WebHostLib/static/static/backgrounds/footer/footer-0005.png index 7b7cd502f36c..794615962454 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0005.png and b/WebHostLib/static/static/backgrounds/footer/footer-0005.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0005.webp b/WebHostLib/static/static/backgrounds/footer/footer-0005.webp new file mode 100644 index 000000000000..c0ee5205ca22 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0005.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass-flowers.png b/WebHostLib/static/static/backgrounds/grass-flowers.png index 464fdbe58155..ea39c5419004 100644 Binary files a/WebHostLib/static/static/backgrounds/grass-flowers.png and b/WebHostLib/static/static/backgrounds/grass-flowers.png differ diff --git a/WebHostLib/static/static/backgrounds/grass-flowers.webp b/WebHostLib/static/static/backgrounds/grass-flowers.webp new file mode 100644 index 000000000000..1b8ebd7706ad Binary files /dev/null and b/WebHostLib/static/static/backgrounds/grass-flowers.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass.png b/WebHostLib/static/static/backgrounds/grass.png index b88c33dec44b..6a99c4d94310 100644 Binary files a/WebHostLib/static/static/backgrounds/grass.png and b/WebHostLib/static/static/backgrounds/grass.png differ diff --git a/WebHostLib/static/static/backgrounds/grass.webp b/WebHostLib/static/static/backgrounds/grass.webp new file mode 100644 index 000000000000..212ab377a624 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/grass.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/dirt-header.png b/WebHostLib/static/static/backgrounds/header/dirt-header.png index 7c9e298e228b..8a9c0963e72f 100644 Binary files a/WebHostLib/static/static/backgrounds/header/dirt-header.png and b/WebHostLib/static/static/backgrounds/header/dirt-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/dirt-header.webp b/WebHostLib/static/static/backgrounds/header/dirt-header.webp new file mode 100644 index 000000000000..6c2b0bd8bf9b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/dirt-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/grass-header.png b/WebHostLib/static/static/backgrounds/header/grass-header.png index c2acc588071c..6d620e5033a2 100644 Binary files a/WebHostLib/static/static/backgrounds/header/grass-header.png and b/WebHostLib/static/static/backgrounds/header/grass-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/grass-header.webp b/WebHostLib/static/static/backgrounds/header/grass-header.webp new file mode 100644 index 000000000000..ca5d1e23bc2b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/grass-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/ocean-header.png b/WebHostLib/static/static/backgrounds/header/ocean-header.png index a0ff51f924f8..1e1c18e93c65 100644 Binary files a/WebHostLib/static/static/backgrounds/header/ocean-header.png and b/WebHostLib/static/static/backgrounds/header/ocean-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/ocean-header.webp b/WebHostLib/static/static/backgrounds/header/ocean-header.webp new file mode 100644 index 000000000000..fc1803ca0e4b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/ocean-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/party-time-header.png b/WebHostLib/static/static/backgrounds/header/party-time-header.png index 799f32f2282e..601ad829f1fa 100644 Binary files a/WebHostLib/static/static/backgrounds/header/party-time-header.png and b/WebHostLib/static/static/backgrounds/header/party-time-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/party-time-header.webp b/WebHostLib/static/static/backgrounds/header/party-time-header.webp new file mode 100644 index 000000000000..0b3c70871ada Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/party-time-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/stone-header.png b/WebHostLib/static/static/backgrounds/header/stone-header.png index e0c9787e5735..f0d2f2fee56e 100644 Binary files a/WebHostLib/static/static/backgrounds/header/stone-header.png and b/WebHostLib/static/static/backgrounds/header/stone-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/stone-header.webp b/WebHostLib/static/static/backgrounds/header/stone-header.webp new file mode 100644 index 000000000000..9f26d1a505d4 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/stone-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/ice.png b/WebHostLib/static/static/backgrounds/ice.png index fcf7299b3582..c64f1b20f3b0 100644 Binary files a/WebHostLib/static/static/backgrounds/ice.png and b/WebHostLib/static/static/backgrounds/ice.png differ diff --git a/WebHostLib/static/static/backgrounds/ice.webp b/WebHostLib/static/static/backgrounds/ice.webp new file mode 100644 index 000000000000..a129d5f439c6 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ice.webp differ diff --git a/WebHostLib/static/static/backgrounds/jungle.png b/WebHostLib/static/static/backgrounds/jungle.png index e27d7e992086..c4ec5b964847 100644 Binary files a/WebHostLib/static/static/backgrounds/jungle.png and b/WebHostLib/static/static/backgrounds/jungle.png differ diff --git a/WebHostLib/static/static/backgrounds/jungle.webp b/WebHostLib/static/static/backgrounds/jungle.webp new file mode 100644 index 000000000000..d21edc8e55f2 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/jungle.webp differ diff --git a/WebHostLib/static/static/backgrounds/ocean.png b/WebHostLib/static/static/backgrounds/ocean.png index 5c22c0b92aa1..d6c9d285c963 100644 Binary files a/WebHostLib/static/static/backgrounds/ocean.png and b/WebHostLib/static/static/backgrounds/ocean.png differ diff --git a/WebHostLib/static/static/backgrounds/ocean.webp b/WebHostLib/static/static/backgrounds/ocean.webp new file mode 100644 index 000000000000..a50b7b27f743 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ocean.webp differ diff --git a/WebHostLib/static/static/backgrounds/party-time.png b/WebHostLib/static/static/backgrounds/party-time.png index ad00851ba4dc..3fcea8a46eef 100644 Binary files a/WebHostLib/static/static/backgrounds/party-time.png and b/WebHostLib/static/static/backgrounds/party-time.png differ diff --git a/WebHostLib/static/static/backgrounds/party-time.webp b/WebHostLib/static/static/backgrounds/party-time.webp new file mode 100644 index 000000000000..7cd547329a40 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/party-time.webp differ diff --git a/WebHostLib/static/static/backgrounds/stone.png b/WebHostLib/static/static/backgrounds/stone.png index 9e15a34375e4..2956beaaa80b 100644 Binary files a/WebHostLib/static/static/backgrounds/stone.png and b/WebHostLib/static/static/backgrounds/stone.png differ diff --git a/WebHostLib/static/static/backgrounds/stone.webp b/WebHostLib/static/static/backgrounds/stone.webp new file mode 100644 index 000000000000..96303c816227 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/stone.webp differ diff --git a/WebHostLib/static/static/branding/header-logo-full.svg b/WebHostLib/static/static/branding/header-logo-full.svg new file mode 100644 index 000000000000..3e22500905f3 --- /dev/null +++ b/WebHostLib/static/static/branding/header-logo-full.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebHostLib/static/static/branding/header-logo.png b/WebHostLib/static/static/branding/header-logo.png index e5d7f9b4a0c0..5a3dbe7dafc5 100644 Binary files a/WebHostLib/static/static/branding/header-logo.png and b/WebHostLib/static/static/branding/header-logo.png differ diff --git a/WebHostLib/static/static/branding/header-logo.svg b/WebHostLib/static/static/branding/header-logo.svg index 3e22500905f3..ceedba43385a 100644 --- a/WebHostLib/static/static/branding/header-logo.svg +++ b/WebHostLib/static/static/branding/header-logo.svg @@ -1,66 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/WebHostLib/static/static/branding/header-logo.webp b/WebHostLib/static/static/branding/header-logo.webp new file mode 100644 index 000000000000..c8088e826266 Binary files /dev/null and b/WebHostLib/static/static/branding/header-logo.webp differ diff --git a/WebHostLib/static/static/branding/landing-logo.png b/WebHostLib/static/static/branding/landing-logo.png index 1f2b967a9844..d4845a475daa 100644 Binary files a/WebHostLib/static/static/branding/landing-logo.png and b/WebHostLib/static/static/branding/landing-logo.png differ diff --git a/WebHostLib/static/static/branding/landing-logo.webp b/WebHostLib/static/static/branding/landing-logo.webp new file mode 100644 index 000000000000..7bd4673e99e0 Binary files /dev/null and b/WebHostLib/static/static/branding/landing-logo.webp differ diff --git a/WebHostLib/static/static/button-images/hamburger-menu-icon.png b/WebHostLib/static/static/button-images/hamburger-menu-icon.png index f1c96316358d..c834501453ab 100644 Binary files a/WebHostLib/static/static/button-images/hamburger-menu-icon.png and b/WebHostLib/static/static/button-images/hamburger-menu-icon.png differ diff --git a/WebHostLib/static/static/button-images/hamburger-menu-icon.webp b/WebHostLib/static/static/button-images/hamburger-menu-icon.webp new file mode 100644 index 000000000000..970754d7bfc8 Binary files /dev/null and b/WebHostLib/static/static/button-images/hamburger-menu-icon.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-a.png b/WebHostLib/static/static/button-images/island-button-a.png index f3872dfd6cdf..552e4d8f6d34 100644 Binary files a/WebHostLib/static/static/button-images/island-button-a.png and b/WebHostLib/static/static/button-images/island-button-a.png differ diff --git a/WebHostLib/static/static/button-images/island-button-a.webp b/WebHostLib/static/static/button-images/island-button-a.webp new file mode 100644 index 000000000000..6da0c1720030 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-a.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-b.png b/WebHostLib/static/static/button-images/island-button-b.png index 65008eaf59ef..fd4a256c7c9f 100644 Binary files a/WebHostLib/static/static/button-images/island-button-b.png and b/WebHostLib/static/static/button-images/island-button-b.png differ diff --git a/WebHostLib/static/static/button-images/island-button-b.webp b/WebHostLib/static/static/button-images/island-button-b.webp new file mode 100644 index 000000000000..6b7c3a279ed0 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-b.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-c.png b/WebHostLib/static/static/button-images/island-button-c.png index 9e5f9f50d2be..2f10f45828c4 100644 Binary files a/WebHostLib/static/static/button-images/island-button-c.png and b/WebHostLib/static/static/button-images/island-button-c.png differ diff --git a/WebHostLib/static/static/button-images/island-button-c.webp b/WebHostLib/static/static/button-images/island-button-c.webp new file mode 100644 index 000000000000..83ce413da807 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-c.webp differ diff --git a/WebHostLib/static/static/button-images/popover.png b/WebHostLib/static/static/button-images/popover.png index cbc863410489..e3247194b06b 100644 Binary files a/WebHostLib/static/static/button-images/popover.png and b/WebHostLib/static/static/button-images/popover.png differ diff --git a/WebHostLib/static/static/button-images/popover.webp b/WebHostLib/static/static/button-images/popover.webp new file mode 100644 index 000000000000..cd1c006221b0 Binary files /dev/null and b/WebHostLib/static/static/button-images/popover.webp differ diff --git a/WebHostLib/static/static/decorations/island-a.png b/WebHostLib/static/static/decorations/island-a.png index d931aed0bdc7..4f5d7c264198 100644 Binary files a/WebHostLib/static/static/decorations/island-a.png and b/WebHostLib/static/static/decorations/island-a.png differ diff --git a/WebHostLib/static/static/decorations/island-a.webp b/WebHostLib/static/static/decorations/island-a.webp new file mode 100644 index 000000000000..32c9cc8f6bd6 Binary files /dev/null and b/WebHostLib/static/static/decorations/island-a.webp differ diff --git a/WebHostLib/static/static/decorations/island-b.png b/WebHostLib/static/static/decorations/island-b.png index d6902281922c..cceb79af33b0 100644 Binary files a/WebHostLib/static/static/decorations/island-b.png and b/WebHostLib/static/static/decorations/island-b.png differ diff --git a/WebHostLib/static/static/decorations/island-b.webp b/WebHostLib/static/static/decorations/island-b.webp new file mode 100644 index 000000000000..3ec6aae438ba Binary files /dev/null and b/WebHostLib/static/static/decorations/island-b.webp differ diff --git a/WebHostLib/static/static/decorations/island-c.png b/WebHostLib/static/static/decorations/island-c.png index 790c7b01d53c..2beedce19d26 100644 Binary files a/WebHostLib/static/static/decorations/island-c.png and b/WebHostLib/static/static/decorations/island-c.png differ diff --git a/WebHostLib/static/static/decorations/island-c.webp b/WebHostLib/static/static/decorations/island-c.webp new file mode 100644 index 000000000000..98e1add91ee3 Binary files /dev/null and b/WebHostLib/static/static/decorations/island-c.webp differ diff --git a/WebHostLib/static/static/decorations/rock-in-water.png b/WebHostLib/static/static/decorations/rock-in-water.png index 25c62acd24fb..1320bef7cee1 100644 Binary files a/WebHostLib/static/static/decorations/rock-in-water.png and b/WebHostLib/static/static/decorations/rock-in-water.png differ diff --git a/WebHostLib/static/static/decorations/rock-in-water.webp b/WebHostLib/static/static/decorations/rock-in-water.webp new file mode 100644 index 000000000000..2c8af460d5e2 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-in-water.webp differ diff --git a/WebHostLib/static/static/decorations/rock-single.png b/WebHostLib/static/static/decorations/rock-single.png index cc237d132ef4..c003abe0d173 100644 Binary files a/WebHostLib/static/static/decorations/rock-single.png and b/WebHostLib/static/static/decorations/rock-single.png differ diff --git a/WebHostLib/static/static/decorations/rock-single.webp b/WebHostLib/static/static/decorations/rock-single.webp new file mode 100644 index 000000000000..e53a2fb5c480 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-single.webp differ 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/static/styles/markdown.css b/WebHostLib/static/styles/markdown.css index e0165b7489ef..5ead2c60f791 100644 --- a/WebHostLib/static/styles/markdown.css +++ b/WebHostLib/static/styles/markdown.css @@ -28,7 +28,7 @@ font-weight: normal; font-family: LondrinaSolid-Regular, sans-serif; text-transform: uppercase; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; text-shadow: 1px 1px 4px #000000; } @@ -37,7 +37,7 @@ font-size: 38px; font-weight: normal; font-family: LondrinaSolid-Light, sans-serif; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; margin-top: 20px; margin-bottom: 0.5rem; @@ -50,7 +50,7 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; text-align: left; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; margin-bottom: 0.5rem; } @@ -59,7 +59,7 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 24px; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ margin-bottom: 24px; } @@ -67,20 +67,29 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 22px; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ } .markdown h6, .markdown details summary.h6{ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 20px; - cursor: pointer;; + cursor: pointer; /* TODO: remove once we drop showdown.js */ } .markdown h4, .markdown h5, .markdown h6{ margin-bottom: 0.5rem; } +.markdown h1 > a, +.markdown h2 > a, +.markdown h3 > a, +.markdown h4 > a, +.markdown h5 > a, +.markdown h6 > a { + color: inherit; +} + .markdown ul{ margin-top: 0.5rem; margin-bottom: 0.5rem; diff --git a/WebHostLib/templates/faq.html b/WebHostLib/templates/faq.html deleted file mode 100644 index 76bdb96d2ef8..000000000000 --- a/WebHostLib/templates/faq.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Frequently Asked Questions - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/gameInfo.html b/WebHostLib/templates/gameInfo.html index c5ebba82848d..3b908004b1be 100644 --- a/WebHostLib/templates/gameInfo.html +++ b/WebHostLib/templates/gameInfo.html @@ -11,7 +11,7 @@ {% block body %} {% include 'header/'+theme+'Header.html' %} -
+
{% endblock %} diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 5a533204083b..b92097ceea08 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -98,15 +98,23 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} + {% elif get_slot_info(team, hint.finding_player).type == 2 %} + {{ 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)] }} + {% elif get_slot_info(team, hint.receiving_player).type == 2 %} + {{ 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/glossary.html b/WebHostLib/templates/glossary.html deleted file mode 100644 index 921f678157fc..000000000000 --- a/WebHostLib/templates/glossary.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Glossary - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 2bbfe4ad0169..c5996d181ee0 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -19,61 +19,186 @@ {% 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"] %} -
- {% endif %} +
{% endblock %} diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 7bbb894de090..b95b8820a72f 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -8,7 +8,7 @@ {%- endmacro %} {% macro list_patches_room(room) %} {% if room.seed.slots %} - +
@@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} - + - - + + diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 415739b861a1..64f0f140de95 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -54,7 +54,7 @@ {% macro NamedRange(option_name, option) %} {{ OptionTitle(option_name, option) }}
- {% for key, val in option.special_range_names.items() %} {% if option.default == val %} @@ -64,17 +64,17 @@ {% endfor %} -
+
- + {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }} {{ RandomizeButton(option_name, option) }} @@ -196,13 +196,14 @@ {% macro OptionTitle(option_name, option) %}
- + - + {% endfor %} @@ -60,16 +78,21 @@

Your Seeds

{% for seed in seeds %} - - + {% endfor %}
Id
{{ patch.player_id }}{{ patch.player_name }}{{ patch.player_name }} {{ patch.game }} {% if patch.data %} diff --git a/WebHostLib/templates/markdown_document.html b/WebHostLib/templates/markdown_document.html new file mode 100644 index 000000000000..07b3c8354d0d --- /dev/null +++ b/WebHostLib/templates/markdown_document.html @@ -0,0 +1,13 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + {% include 'header/grassHeader.html' %} + {{ title }} + +{% endblock %} + +{% block body %} +
+ {{ html_from_markdown | safe}} +
+{% endblock %} diff --git a/WebHostLib/templates/multitrackerHintTable.html b/WebHostLib/templates/multitrackerHintTable.html index a931e9b04845..fcc15fb37a9f 100644 --- a/WebHostLib/templates/multitrackerHintTable.html +++ b/WebHostLib/templates/multitrackerHintTable.html @@ -21,8 +21,20 @@ ) -%}
{{ player_names_with_alias[(team, hint.finding_player)] }}{{ player_names_with_alias[(team, hint.receiving_player)] }} + {% if get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} + + {% if get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} + {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} {{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }} {{ games[(team, hint.finding_player)] }}
{{ 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.
{{ 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.
{% else %} - You have no generated any seeds yet! + You have not generated any seeds yet! {% endif %}
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index a1d319697154..d18d0f0b8957 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -53,7 +53,7 @@ {{ RangeRow(option_name, option, option.range_start, option.range_start, True) }} - {% if option.range_start < option.default < option.range_end %} + {% if option.default is number and option.range_start < option.default < option.range_end %} {{ RangeRow(option_name, option, option.default, option.default, True) }} {% endif %} {{ RangeRow(option_name, option, option.range_end, option.range_end, True) }} @@ -138,7 +138,7 @@ id="{{ option_name }}-{{ key }}" name="{{ option_name }}||{{ key }}" value="1" - checked="{{ "checked" if key in option.default else "" }}" + {{ "checked" if key in option.default }} />
\n" + for (name, description) in sorted( + location_names_to_descriptions.items(), + key = lambda pair: pair[0] + ): + table += f"\n" + table += "
Location nameDetailed description
{html.escape(name)}{html.escape(description)}
\n" + + with open( + os.path.join(os.path.dirname(__file__), 'docs/locations_en.md'), + 'r+', + encoding='utf-8' + ) as f: + original = f.read() + start_flag = "\n" + start = original.index(start_flag) + len(start_flag) + end = original.index("") + + f.seek(0) + f.write(original[:start] + table + original[end:]) + f.truncate() + + print("Updated docs/locations_en.md!") diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index f31358bb9c2f..06227226aafe 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -1,28 +1,201 @@ # Dark Souls III +Game Page | [Items] | [Locations] + +[Items]: /tutorial/Dark%20Souls%20III/items/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## What do I need to do to randomize DS3? + +See full instructions on [the setup page]. + +[the setup page]: /tutorial/Dark%20Souls%20III/setup/en + ## Where is the options page? -The [player options page for this game](../player-options) contains all the options you need to configure and export a -config file. +The [player options page for this game][options] contains all the options you +need to configure and export a config file. + +[options]: ../player-options ## What does randomization do to this game? -Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be -randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the -location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what -happens when you randomize Estus Shards and Undead Bone Shards. +1. All item locations are randomized, including those in the overworld, in + shops, and dropped by enemies. Most locations can contain games from other + worlds, and any items from your world can appear in other players' worlds. + +2. By default, all enemies and bosses are randomized. This can be disabled by + setting "Randomize Enemies" to false. + +3. By default, the starting equipment for each class is randomized. This can be + disabled by setting "Randomize Starting Loadout" to false. + +4. By setting the "Randomize Weapon Level" or "Randomize Infusion" options, you + can randomize whether the weapons you find will be upgraded or infused. + +There are also options that can make playing the game more convenient or +bring a new experience, like removing equip loads or auto-equipping weapons as +you pick them up. Check out [the options page][options] for more! + +## What's the goal? + +Your goal is to find the four "Cinders of a Lord" items randomized into the +multiworld and defeat the boss in the Kiln of the First Flame. + +## Do I have to check every item in every area? + +Dark Souls III has about 1500 item locations, which is a lot of checks for a +single run! But you don't necessarily need to check all of them. Locations that +you can potentially miss, such as rewards for failable quests or soul +transposition items, will _never_ have items required for any game to progress. +The following types of locations are also guaranteed not to contain progression +items by default: + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +You can customize which locations are guaranteed not to contain progression +items by setting the `exclude_locations` field in your YAML to the [location +groups] you want to omit. For example, this is the default setting but without +"Hidden" so that hidden locations can contain progression items: + +[location groups]: /tutorial/Dark%20Souls%20III/locations/en#location-groups + +```yaml +Dark Souls III: + exclude_locations: + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +This allows _all_ non-missable locations to have progression items, if you're in +for the long haul: + +```yaml +Dark Souls III: + exclude_locations: [] +``` + +## What if I don't want to do the whole game? + +If you want a shorter DS3 randomizer experience, you can exclude entire regions +from containing progression items. The items and enemies from those regions will +still be included in the randomization pool, but none of them will be mandatory. +For example, the following configuration just requires you to play the game +through Irithyll of the Boreal Valley: + +```yaml +Dark Souls III: + # Enable the DLC so it's included in the randomization pool + enable_dlc: true + + exclude_locations: + # Exclude late-game and DLC regions + - Anor Londo + - Lothric Castle + - Consumed King's Garden + - Untended Graves + - Grand Archives + - Archdragon Peak + - Painted World of Ariandel + - Dreg Heap + - Ringed City + + # Default exclusions + - Hidden + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +## Where can I learn more about Dark Souls III locations? + +Location names have to pack a lot of information into very little space. To +better understand them, check out the [location guide], which explains all the +names used in locations and provides more detailed descriptions for each +individual location. + +[location guide]: /tutorial/Dark%20Souls%20III/locations/en + +## Where can I learn more about Dark Souls III items? + +Check out the [item guide], which explains the named groups available for items. + +[item guide]: /tutorial/Dark%20Souls%20III/items/en + +## What's new from 2.x.x? + +Version 3.0.0 of the Dark Souls III Archipelago client has a number of +substantial differences with the older 2.x.x versions. Improvements include: + +* Support for randomizing all item locations, not just unique items. + +* Support for randomizing items in shops, starting loadouts, Path of the Dragon, + and more. + +* Built-in integration with the enemy randomizer, including consistent seeding + for races. + +* Support for the latest patch for Dark Souls III, 1.15.2. Older patches are + *not* supported. + +* Optional smooth distribution for upgrade items, upgraded weapons, and soul + items so you're more likely to see weaker items earlier and more powerful + items later. + +* More detailed location names that indicate where a location is, not just what + it replaces. + +* Other players' item names are visible in DS3. + +* If you pick up items while static, they'll still send once you reconnect. + +However, 2.x.x YAMLs are not compatible with 3.0.0. You'll need to [generate a +new YAML configuration] for use with 3.x.x. + +[generating a new YAML configuration]: /games/Dark%20Souls%20III/player-options + +The following options have been removed: + +* `enable_boss_locations` is now controlled by the `soul_locations` option. + +* `enable_progressive_locations` was removed because all locations are now + individually randomized rather than replaced with a progressive list. + +* `pool_type` has been removed. Since there are no longer any non-randomized + items in randomized categories, there's not a meaningful distinction between + "shuffle" and "various" mode. -It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have -one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as -removing weapon requirements or auto-equipping whatever equipment you most recently received. +* `enable_*_locations` options have all been removed. Instead, you can now add + [location group names] to the `exclude_locations` option to prevent them from + containing important items. -The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder. + [location group names]: /tutorial/Dark%20Souls%20III/locations/en#location-groups -## What Dark Souls III items can appear in other players' worlds? + By default, the Hidden, Small Crystal Lizards, Upgrade, Small Souls, and + Miscellaneous groups are in `exclude_locations`. Once you've chosen your + excluded locations, you can set `excluded_locations: unrandomized` to preserve + the default vanilla item placements for all excluded locations. -Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables, -spells, upgrade materials, etc... +* `guaranteed_items`: In almost all cases, all items from the base game are now + included somewhere in the multiworld. -## What does another world's item look like in Dark Souls III? +In addition, the following options have changed: -In Dark Souls III, items which are sent to other worlds appear as Prism Stones. +* The location names used in options like `exclude_locations` have changed. See + the [location guide] for a full description. diff --git a/worlds/dark_souls_3/docs/items_en.md b/worlds/dark_souls_3/docs/items_en.md new file mode 100644 index 000000000000..b9de5e500a96 --- /dev/null +++ b/worlds/dark_souls_3/docs/items_en.md @@ -0,0 +1,24 @@ +# Dark Souls III Items + +[Game Page] | Items | [Locations] + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## Item Groups + +The Dark Souls III randomizer supports a number of item group names, which can +be used in YAML options like `local_items` to refer to many items at once: + +* **Progression:** Items which unlock locations. +* **Cinders:** All four Cinders of a Lord. Once you have these four, you can + fight Soul of Cinder and win the game. +* **Miscellaneous:** Generic stackable items, such as arrows, firebombs, buffs, + and so on. +* **Unique:** Items that are unique per NG cycle, such as scrolls, keys, ashes, + and so on. Doesn't include equipment, spells, or souls. +* **Boss Souls:** Souls that can be traded with Ludleth, including Soul of + Rosaria. +* **Small Souls:** Soul items, not including boss souls. +* **Upgrade:** Upgrade items, including titanite, gems, and Shriving Stones. +* **Healing:** Undead Bone Shards and Estus Shards. diff --git a/worlds/dark_souls_3/docs/locations_en.md b/worlds/dark_souls_3/docs/locations_en.md new file mode 100644 index 000000000000..8411b8c42aa0 --- /dev/null +++ b/worlds/dark_souls_3/docs/locations_en.md @@ -0,0 +1,2276 @@ +# Dark Souls III Locations + +[Game Page] | [Items] | Locations + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Items]: /tutorial/Dark%20Souls%20III/items/en + +## Table of Contents + +* [Location Groups](#location-groups) +* [Understanding Location Names](#understanding-location-names) + * [HWL: High Wall of Lothric](#high-wall-of-lothric) + * [US: Undead Settlement](#undead-settlement) + * [RS: Road of Sacrifices](#road-of-sacrifices) + * [CD: Cathedral of the Deep](#cathedral-of-the-deep) + * [FK: Farron Keep](#farron-keep) + * [CC: Catacombs of Carthus](#catacombs-of-carthus) + * [SL: Smouldering Lake](#smouldering-lake) + * [IBV: Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) + * [ID: Irithyll Dungeon](#irithyll-dungeon) + * [PC: Profaned Capital](#profaned-capital) + * [AL: Anor Londo](#anor-londo) + * [LC: Lothric Castle](#lothric-castle) + * [CKG: Consumed King's Garden](#consumed-kings-garden) + * [GA: Grand Archives](#grand-archives) + * [UG: Untended Graves](#untended-graves) + * [AP: Archdragon Peak](#archdragon-peak) + * [PW1: Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) + * [PW2: Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) + * [DH: Dreg Heap](#dreg-heap) + * [RC: Ringed City](#ringed-city) +* [Detailed Location Descriptions](#detailed-location-descriptions) + +## Location Groups + +The Dark Souls III randomizer supports a number of location group names, which +can be used in YAML options like `exclude_locations` to refer to many locations +at once: + +* **Prominent:** A small number of locations that are in very obvious locations. + Mostly boss drops. Ideal for setting as priority locations. + +* **Progression:** Locations that contain items in vanilla which unlock other + locations. + +* **Boss Rewards:** Boss drops. Does not include soul transfusions or shop + items. + +* **Miniboss Rewards:** Miniboss drops. Minibosses are large enemies that don't + respawn after being killed and usually drop some sort of treasure, such as + Boreal Outrider Knights and Ravenous Crystal Lizards. Only includes enemies + considered minibosses by the enemy randomizer. + +* **Mimic Rewards:** Drops from enemies that are mimics in vanilla. + +* **Hostile NPC Rewards:** Drops from NPCs that are hostile to you. This + includes scripted invaders and initially-friendly NPCs that must be fought as + part of their quest. + +* **Friendly NPC Rewards:** Items given by friendly NPCs as part of their quests + or from non-violent interaction. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Boss Souls:** Locations that contain boss souls in vanilla, as well as Soul + of Rosaria. + +* **Unique:** Locations that contain items in vanilla that are unique per NG + cycle, such as scrolls, keys, ashes, and so on. Doesn't cover equipment, + spells, or souls. + +* **Healing:** Locations that contain Undead Bone Shards and Estus Shards in + vanilla. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Weapons:** Locations that contain weapons in vanilla. + +* **Shields:** Locations that contain shields in vanilla. + +* **Armor:** Locations that contain armor in vanilla. + +* **Rings:** Locations that contain rings in vanilla. + +* **Spells:** Locations that contain spells in vanilla. + +## Understanding Location Names + +All locations begin with an abbreviation indicating their general region. Most +locations have a set of landmarks that are used in location names to keep them +short. + +* **FS:** Firelink Shrine +* **FSBT:** Firelink Shrine belltower +* **HWL:** [High Wall of Lothric](#high-wall-of-lothric) +* **US:** [Undead Settlement](#undead-settlement) +* **RS:** [Road of Sacrifices](#road-of-sacrifices) +* **CD:** [Cathedral of the Deep](#cathedral-of-the-deep) +* **FK:** [Farron Keep](#farron-keep) +* **CC:** [Catacombs of Carthus](#catacombs-of-carthus) +* **SL:** [Smouldering Lake](#smouldering-lake) +* **IBV:** [Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) +* **ID:** [Irithyll Dungeon](#irithyll-dungeon) +* **PC:** [Profaned Capital](#profaned-capital) +* **AL:** [Anor Londo](#anor-londo) +* **LC:** [Lothric Castle](#lothric-castle) +* **CKG:** [Consumed King's Garden](#consumed-kings-garden) +* **GA:** [Grand Archives](#grand-archives) +* **UG:** [Untended Graves](#untended-graves) +* **AP:** [Archdragon Peak](#archdragon-peak) +* **PW1:** [Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) +* **PW2:** [Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) +* **DH:** [Dreg Heap](#dreg-heap) +* **RC:** [Ringed City](#ringed-city) + +General notes: + +* "Lizard" always refers to a small crystal lizard. + +* "Miniboss" are large enemies that don't respawn after being killed and usually + drop some sort of treasure, such as Boreal Outrider Knights and Ravenous + Crystal Lizards. + +* NPC quest items are always in the first location you can get them _without_ + killing the NPC or ending the quest early. + +### High Wall of Lothric + +* **Back tower:** The tower _behind_ the High Wall of Lothric bonfire, past the + path to the shortcut elevator. + +* **Corpse tower:** The first tower after the High Wall of Lothric bonfire, with + a dead Wyvern on top of it. + +* **Fire tower:** The second tower after the High Wall of Lothric bonfire, where + a living Wyvern lands and breathes fire at you. + +* **Flame plaza:** The open area with many items where the Wyvern breathes fire. + +* **Wall tower:** The third tower after the High Wall of Lothric bonfire, with + the Tower on the Wall bonfire. + +* **Fort:** The large building after the Tower on the Wall bonfire, with the + transforming hollow on top. + + * "Entry": The first room you enter after descending the ladder from the roof. + + * "Walkway": The top floor of the tall room, with a path around the edge + hidden by a large wheel. + + * "Mezzanine": The middle floor of the tall room, with a chest. + + * "Ground": The bottom floor of the tall room, with an anvil and many mobs. + +* **Fountain:** The large fountain with many dead knights around it, where the + Winged Knight patrols in vanilla. + +* **Shortcut:** The unlockable path between the promenade and the High Wall of + Lothric bonfire, including both the elevator and the area at its base. + +* **Promenade:** The long, wide path between the two boss arenas. + +### Undead Settlement + +* **Foot:** The area where you first appear, around the Foot of the High Wall + bonfire. + +* **Burning tree:** The tree near the beginning of the region, with the + Cathedral Evangelist in front of it in vanilla. + +* **Hanging corpse room:** The dark room to the left of the burning tree with + many hanging corpses inside, on the way to the Dilapidated Bridge bonfire. + +* **Back alley:** The path between buildings leading to the Dilapidated Bridge + bonfire. + +* **Stable:** The building complex across the bridge to the right of the burning + tree. + +* **White tree:** The birch tree by the Dilapidated Bridge bonfire, where the + giant shoots arrows. + +* **Sewer:** The underground passage between the chasm and the Dilapidated + Bridge bonfire. + +* **Chasm:** The chasm underneath the bridge on the way to the tower. It's + possible to get into the chasm without a key by dropping down next to Eygon of + Carim with a full health bar. + +* **Tower:** The tower at the end of the region with the giant archer at the + top. + +* **Tower village:** The village reachable from the tower, where the Fire Demon + patrols in vanilla. + +### Road of Sacrifices + +The area after the Crystal Sage is considered part of the Cathedral of the Deep +region. + +* **Road:** The path from the Road of Sacrifices bonfire to the Halfway Fortress + bonfire. + +* **Woods:** The wooded area on land, after the Halfway Fortress bonfire and + surrounding the Crucifixion Woods bonfire. + +* **Water:** The watery area, covered in crabs in vanilla. + +* **Deep water:** The area in the water near the ladder to Farron Keep, where + your walking is slowed. + +* **Stronghold:** The stone building complex on the way to Crystal Sage. + + * "Left room" is the room whose entrance is near the Crucifixion Woods + bonfire. + + * "Right room" is the room up the stairs closer to Farron Keep. + +* **Keep perimeter:** The building with the Black Knight and the locked door to + the Farron Keep Perimeter bonfire. + +### Cathedral of the Deep + +* **Path:** The path from Road of Sacrifices to the cathedral proper. + +* **Moat:** The circular path around the base of the front of the + cathedral, with the Ravenous Crystal Lizard and Corpse-Grubs in vanilla. + +* **Graveyard:** The area with respawning enemies up the hill from the Cleansing + Chapel bonfire. + +* **White tree:** The birch tree below the front doors of the chapel and across + the moat from the graveyard, where the giant shoots arrows if he's still + alive. + +* **Lower roofs:** The roofs, flying buttresses, and associated areas to the + right of the front door, which must be traversed before entering the + cathedral. + +* **Upper roofs:** The roofs, flying buttresses, and rafters leading to the + Rosaria's Bedchamber bonfire. + +* **Main hall:** The central and largest room in the cathedral, with the muck + that slows your movement. Divided into the south (with the sleeping giant in + vanilla) and east (with many items) wings, with north pointing towards the + door to the boss. + +* **Side chapel:** The room with rows of pews and the patrolling Cathedral + Knight in vanilla, to the side of the main hall. + +### Farron Keep + +* **Left island:** The large island with the ritual flame, to the left as you + leave the Farron Keep bonfire. + +* **Right island:** The large island with the ritual flame, to the right as you + leave the Farron Keep bonfire. + +* **Hidden cave:** A small cave in the far corner of the map, closest to the + right island. Near a bunch of basilisks in vanilla. + +* **Keep ruins:** The following two islands: + + * "Bonfire island": The island with the Keep Ruins bonfire. + * "Ritual island": The island with one of the three ritual fires. + +* **White tree**: The birch tree by the ramp down from the keep ruins bonfire + island, where the giant shoots arrows if he's still alive. + +* **Keep proper:** The building with the Old Wolf of Farron bonfire. + +* **Upper keep:** The area on top of the keep proper, reachable from the + elevator from the Old Wolf of Farron bonfire. + +* **Perimeter:** The area from near the Farron Keep Perimeter bonfire, including + the stone building and the path to the boss. + +### Catacombs of Carthus + +All the area up to the Small Doll wall into Irithyll is considered part of the +Catacombs of Carthus region. + +* **Atrium:** The large open area you first enter and the rooms attached to it. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the short stairs but at the top of the long + stairway that the skeleton ball rolls down. + +* **Crypt:** The enclosed area at the bottom of the long stairway that the + skeleton ball rolls down. + + * "Upper" is the floor the long stairway leads to that also contains the + Catacombs of Carthus bonfire. + * "Lower" is the floor with rats and bonewheels in vanilla. + * "Across" is the area reached by going up the set of stairs across from + the entrance downstairs. + +* **Cavern:** The even larger open area past the crypt with the rope bridge to + the boss arena. + +* **Tomb:** The area on the way to Smouldering Lake, reachable by cutting down + the rope bridge and climbing down it. + +* **Irithyll Bridge:** The outdoor bridge leading to Irithyll of the Boreal + Valley. + +### Smouldering Lake + +* **Lake:** The watery area you enter initially, where you get shot at by the + ballista. + +* **Side lake:** The small lake accessible via a passage from the larger one, in + which you face Horace the Hushed as part of his quest. + +* **Ruins main:** The area you first enter after the Demon Ruins bonfire. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the stairs. + +* **Antechamber:** The area up the flight of stairs near the +Old King's Antechamber bonfire. + +* **Ruins basement:** The area further down from ruins main lower, with many + basilisks and Knight Slayer Tsorig in vanilla. + +### Irithyll of the Boreal Valley + +This region starts _after_ the Small Doll wall and ends with Pontiff Sulyvahn. +Everything after that, including the contents of Sulyvahn's cathedral is +considered part of Anor Londo. + +* **Central:** The beginning of the region, from the Central Irithyll bonfire up + to the plaza. + +* **Dorhys:** The sobbing mob (a Cathedral Evangelist in vanilla) behind the + locked door opening onto central. Accessed through an illusory railing by the + crystal lizard just before the plaza. + +* **Plaza:** The area in front of and below the cathedral, with a locked door up + to the cathedral and a locked elevator to the Ascent. + +* **Descent:** The path from the Church of Yorshka bonfire down to the lake. + +* **Lake:** The open watery area outside the room with the Distant Manor + bonfire. + +* **Sewer:** The room between the lake and the beginning of the ascent, filled + with Sewer Centipedes in vanilla. + +* **Ascent:** The path up from the lake to the cathedral, through several + buildings and some open stairs. + +* **Great hall:** The building along the ascent with a large picture of + Gwynevere and several Silver Knights in vanilla. + +### Irithyll Dungeon + +In Irithyll Dungeon locations, "left" and "right" are always oriented as though +"near" is where you stand and "far" is where you're facing. (For example, you +enter the dungeon from the bonfire on the near left.) + +* **B1:** The floor on which the player enters the dungeon, with the Irithyll + Dungeon bonfire. + + * "Near" is the side of the dungeon with the bonfire. + * "Far" is the opposite side. + +* **B2:** The floor directly below B1, which can be reached by going down the + stairs or dropping. + + * "Near" is the same side of the dungeon as the bonfire. + * "Far" is the opposite side. + +* **Pit:** The large room with the Giant Slave and many Rats in vanilla. + +* **Pit lift:** The elevator from the pit up to B1 near, right to the Irithyll + Dungeon bonfire. + +* **B3:** The lowest floor, with Karla's cell, a lift back to B2, and the exit + onwards to the Profaned Capital. + + * "Near" is the side with Karla's cell and the path from the pit. + * "Far" is the opposite side with the mimic. + +* **B3 lift:** The elevator from B3 (near where you can use Path of the Dragon + to go to Archdragon Peak) up to B2. + +### Profaned Capital + +* **Tower:** The tower that contains the Profaned Capital bonfire. + +* **Swamp:** The pool of toxic liquid accessible by falling down out of the + lower floor of the tower, going into the corridor to the left, and falling + down a hole. + +* **Chapel:** The building in the swamp containing Monstrosities of Sin in + vanilla. + +* **Bridge:** The long bridge from the tower into the palace. + +* **Palace:** The large building carved into the wall of the cavern, full of + chalices and broken pillars. + +### Anor Londo + +This region includes everything after Sulyvahn's cathedral, including its upper +story. + +* **Light cathedral:** The cathedral in which you fight Pontiff Sulyvahn in + vanilla. + +* **Plaza:** The wide open area filled with Giant Slaves in vanilla. + +* **Walkway:** The path above the plaza leading to the second floor of the light + cathedral, with Deacons in vanilla. + +* **Buttresses:** The flying buttresses that you have to climb to get to the + spiral staircase. "Near" and "far" are relative to the light cathedral, so the + nearest buttress is the one that leads back to the walkway. + +* **Tomb:** The area past the illusory wall just before the spiral staircase, in + which you marry Anri during Yoel and Yuria's quest. + +* **Dark cathedral:** The darkened cathedral just before the Aldrich fight in + vanilla. + +### Lothric Castle + +This region covers everything up the ladder from the Dancer of the Boreal Valley +bonfire up to the door into Grand Archives, except the area to the left of the +ladder which is part of Consumed King's Garden. + +* **Lift:** The elevator from the room straight after the Dancer of the Boreal + Valley bonfire up to just before the boss fight. + +* **Ascent:** The set of stairways and turrets leading from the Lothric Castle + bonfire to the Dragon Barracks bonfire. + +* **Barracks:** The large building with two fire-breathing wyverns across from + the Dragon Barracks bonfire. + +* **Moat:** The ditch beneath the bridge leading to the barracks. + + * The "right path" leads to the right as you face the barracks, around and + above the stairs up to the Dragon Barracks bonfire. + +* **Plaza:** The open area in the center of the barracks, where the two wyverns + breathe fire. + + * "Left" is the enclosed area on the left as you're coming from the Dragon + Barracks bonfire, with the stairs down to the basement. + +* **Basement:** The room beneath plaza left, with the Boreal Outrider in + vanilla. + +* **Dark room:** The large darkened room on the right of the barracks as you're + coming from the Dragon Barracks bonfire, with firebomb-throwing Hollows in + vanilla. + + * "Lower" is the bottom floor that you enter onto from the plaza. + * "Upper" is the top floor with the door to the main hall. + * "Mid" is the middle floor accessible by climbing a ladder from lower or + going down stairs from upper. + +* **Main hall:** The central room of the barracks, behind the gate. + +* **Chapel:** The building to the right just before the stairs to the boss, with + a locked elevator to Grand Archives. + +* **Wyvern room:** The room where you can fight the Pus of Man infecting the + left wyvern, accessible by dropping down to the left of the stairs to the + boss. + +* **Altar:** The building containing the Altar of Sunlight, accessible by + climbing up a ladder onto a roof around the corner from the stairs to the + boss. + +### Consumed King's Garden + +This region covers everything to the left of the ladder up from the Dancer of +the Boreal Valley bonfire up to the illusory wall into Untended Graves. + +* **Balcony:** The walkway accessible by getting off the first elevator halfway + down. + +* **Rotunda:** The building in the center of the toxic pool, with a Cathedral + Knight on it in vanilla. + +* **Lone stairway:** A set of stairs leading nowhere in the far left of the main + area as you enter from the first elevator. + +* **Shortcut:** The path from the locked door into Lothric Castle, through the + room filled with thralls in vanilla, and down a lift. + +* **Tomb:** The area after the boss room. + +### Grand Archives + +* **1F:** The first floor of the Grand Archives, including the first wax pool. + +* **Dark room:** The unlit room on 1F to the right of the wax pool. + +* **2F:** The second floor of the grand archives. It's split into two sections + that are separated by retractable bookshelves. + + * "Early" is the first part you reach and has an outdoor balcony with a ladder + to 3F and a wax pool up a short set of stairs. + * "Late" is the part you can only reach by climbing down from F3, where you + encounter the teleporting miniboss for the final time. + +* **3F:** The third floor of the grand archives, where you encounter the + teleporting miniboss for the second time. Includes the area with a hidden room + with another miniboss. + +* **4F:** The topmost and most well-lit section of bookshelves, overlooking the + rest of the archives. + +* **Rooftops:** The outer rooftop area between 4F and 5F, with Gargoyles in + vanilla. + + * "Lower" is the balcony you can reach by dropping off the rooftops, as well + as the further rooftops leading down to the 2F early balcony. + +* **5F:** The topmost floor of the archives interior, accessible from the + rooftops, with a ladder down to 4F. + +* **Dome:** The domed roof of the Grand Archives, with Ascended Winged Knights + in vanilla. + +* **Rafters:** The narrow walkways above the Grand Archives, accessible by + dropping down from the dome. + +### Untended Graves + +* **Swamp:** The watery area immediately after the Untended graves bonfire, up + to the cemetery. + +* **Cemetery:** The area past where the Cemetery of Ash bonfire would be, up to + the boss arena. + +* **Environs:** The area after the boss and outside the abandoned Firelink + Shrine. + +* **Shrine:** The area inside the abandoned Firelink Shrine. + +### Archdragon Peak + +"Gesture" always means the Path of the Dragon gesture. + +* **Intro:** The first section, from where you warp in from Irithyll Dungeon up + to the first boss fight. + + * "Archway": The large stone archway in front of the boss door. + +* **Fort:** The arena where you fight Ancient Wyvern in vanilla. + + * "Overlook": The area down the stairs from where the Ancient Wyvern first + lands in vanilla, overlooking the fog. + + * "Rotunda": The top of the spiral staircase building, to the left before the + bridge with the chain-axe Man-Serpent in vanilla. + +* **Mausoleum:** The building with the Dragon-Kin Mausoleum bonfire, where + you're warped after the first boss fight. + +* **Walkway:** The path from the mausoleum to the belfry, looking out over + clouds. + + * "Building": The building along the walkway, just before the wyvern in + vanilla. + +* **Belfry:** The building with the Great Belfry bonfire, including the room + with the summoner. + +* **Plaza:** The arena that appears after you defeat Nameless King in vanilla. + +* **Summit:** The path up from the belfry to the final altar at the top of the + mountain. + +### Painted World of Ariandel (Before Contraption) + +This region covers the Ashes of Ariandel DLC up to the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter. + +* **Snowfield:** The area around the Snowfield bonfire, + + * "Upper": The area immediately after the Snowfield bonfire, before the + collapsing overhang, with the Followers in vanilla. + + * "Lower": The snowy tree-filled area after the collapsing overhang, with the + Wolves in vanilla. + + * "Village": The area with broken-down buildings and Millwood Knights in + vanilla. + + * "Tower": The tower by the village, with Millwood Knights in Vanilla. + +* **Bridge:** The rope bridge to the chapel. + + * "Near": The side of the bridge by the Rope Bridge Cave bonfire. + + * "Far": The side of the bridge by the Ariandel Chapel bonfire. + +* **Chapel:** The building with the Ariandel Chapel bonfire and Lady Friede. + +* **Depths:** The area reachable by cutting down the bridge and descending on + the far side, with the Depths of the Painting bonfire. + +* **Settlement:** The area reachable by cutting down the bridge and descending + on the near side, with the Corvian Settlement bonfire. Everything after the + slide down the hill is considered part of the settlement. + + * "Courtyard": The area in front of the settlement, immediately after the + slide. + + * "Main": The main road of the settlement leading up to the locked gate to the + library. Also includes the buildings that are immediately accessible from + this road. + + * "Loop": A side path that loops left from the main road and goes up and + behind the building with the bonfire. + + * "Back": The back alley of the settlement, accessible by dropping down to the + right of the locked gate to the library. Also includes the buildings that + are immediately accessible from this alley. + + * "Roofs": The village rooftops, first accessible by climbing a ladder from + the back alley. Also includes the buildings and items that are first + accessible from the roofs. + + * "Hall": The largest building in the settlement, with two Corvian Knights in + vanilla. + +* **Library:** The building where you use the contraption key, where Vilhelm + appears in vanilla. + +### Painted World of Ariandel (After Contraption) + +This region covers the Ashes of Ariandel DLC past the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter, including the basement beneath the chapel. + +* **Pass:** The mountainous area past the Snowy Mountain Pass bonfire. + +* **Pit:** The area with a large tree and numerous Millwood Knights in vanilla, + reached by a collapsing overhang in the pass. + +* **B1:** The floor immediately below the chapel, first accessible from the + pass. Filled with Giant Flies in vanilla. + +* **B2:** The floor below B1, with lots of fly eggs. Filled with even more Giant + Flies than B1 in vanilla. + +* **B3:** The floor below B2, accessible through an illusory wall. + +* **Rotunda:** The round arena out in the open, accessible by platforming down + tree roots from B3. + +### Dreg Heap + +* **Shop:** Items sold by the Stone-Humped Hag by The Dreg Heap bonfire. + +* **Castle:** The building with The Dreg Heap bonfire, up to the large fall into + the library. + +* **Library:** The building with the stained-glass window that you fall into + from the castle. + +* **Church:** The building below and to the right of the library, which the + pillar falls into to make a bridge. + +* **Pantry:** The set of rooms entered through a door near the fountain just + past the church, with boxes and barrels. + + * "Upstairs": The room with an open side, accessible through an illusory wall + in the furthest pantry room. + +* **Parapets:** The area with balconies and Overgrown Lothric Knights in + vanilla, accessible by taking the pillar bridge from the church, following + that path to the end, and dropping down to the right. + +* **Ruins:** The area around the Earthen Peak Ruins bonfire, up to the swamp. + +* **Swamp:** The area in and above the poisonous water, up to the point the + branches deposit you back on the ruins. + + * "Left": Left as you enter from the ruins, towards the cliff edge. + + * "Right": Right as you enter from the ruins, towards higher ground. + + * "Upper": The path up and over the swamp towards the Within Earthen Peak + Ruins bonfire. + +### Ringed City + +The "mid boss", "end boss", and "hidden boss" are the bosses who take the place +of Halflight, Gael, and Midir, respectively. + +* **Wall:** The large wall in which you spawn when you first enter the area, + with the Mausoleum Lookout bonfire. + + * "Top": The open-air top of the wall, where you first spawn in. + + * "Upper": The upper area of the wall, with the Ringed Inner Wall bonfire. + + * "Tower": The tiered tower leading down from the upper area to the stairs. + + * "Lower": The lower rooms of the wall, accessible from the lower cliff, with + an elevator back to upper. + + * "Hidden": The hidden floor accessible from the elevator from lower to upper, + from which you can reach Midir in vanilla. + +* **Streets:** The streets and skyways of the city proper. "Left" and "right" + are relative to the main staircase as you head down towards the swamp, "near" + and "far" are relative to Shira's chamber at the top of the stairs. + + * "Garden": The flower-filled back alley accessible from the left side of the + nearest bridge over the stairs. + + * "High": The higher areas in the far left where you can find the Locust + Preacher, accessible from a long ladder in the swamp. + + * "Monument": The area around the purging monument, which can only be accessed + by solving the "Show Your Humanity" puzzle. + +* **Swamp:** The wet area past the city streets. "Left" and "right" are relative + to heading out from the Ringed City Streets bonfire, and "near" and "far" are + relative to that bonfire as well. + +* **Lower cliff:** The cliffside path leading from the swamp into the shared + grave, where Midir breathes fire. + +* **Grave:** The cylindrical chamber with spiral stairs around the edges, + connecting the two cliffs, containing the Shared Grave bonfire. + +* **Upper cliff:** The cliffside path leading out of the grave to the lower + wall. + +* **Church path:** The sunlit path from the lower cliff up to the Church of + Filianore where you fight Halflight in vanilla. + +* **Ashes:** The final area, where you fight Gael in vanilla. + +## Detailed Location Descriptions + +These location descriptions were originally written by [Matt Gruen] for [the +static _Dark Souls III_ randomizer]. + +[Matt Gruen]: https://thefifthmatt.com/ +[the static _Dark Souls III_ randomizer]: https://www.nexusmods.com/darksouls3/mods/361 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Location nameDetailed description
AL: Aldrich Faithful - water reserves, talk to McDonnelGiven by Archdeacon McDonnel in Water Reserves.
AL: Aldrich's Ruby - dark cathedral, minibossDropped by the Deep Accursed who drops down when you open the Anor Londo Cathedral shortcut
AL: Anri's Straight Sword - Anri questDropped by Anri of Astora upon death or completing quest. In the Darkmoon Tomb with Lord of Hollows route, or given by Ludleth if summoned to defeat Aldrich.
AL: Blade of the Darkmoon - Yorshka with Darkmoon LoyaltyGiven by Yorshka after learning the Darkmoon Loyalty gesture from Sirris, or by killing her
AL: Brass Armor - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Gauntlets - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Helm - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Leggings - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Chameleon - tomb after marrying AnriDropped by the Stone-humped Hag assassin after Anri reaches the Church of Yorshka, either in the church or after marrying Anri
AL: Cinders of a Lord - AldrichDropped by Aldrich
AL: Crescent Moon Sword - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Dark Stoneplate Ring - by dark stairs up from plazaAfter the Pontiff fight, in the dark hallways to the left of the area with the Giant Slaves
AL: Deep Gem - water reservesIn the open in the Water Reserves
AL: Dragonslayer Greatarrow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Dragonslayer Greatbow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Drang Twinspears - plaza, NPC dropDropped by Drang Twinspears-wielding knight on the stairs leading up to the Anor Londo Silver Knights
AL: Easterner's Ashes - below top of furthest buttressDropping down from the rightmost flying buttress, or the rightmost set of stairs
AL: Ember - plaza, furtherAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Ember - plaza, right sideAfter the Pontiff fight, next to one of the Giant Slaves on the right side
AL: Ember - spiral staircase, bottomNext to the lever that summons the rotating Anor Londo stairs at the bottom
AL: Estus Shard - dark cathedral, by left stairsIn a chest on the floor of the Anor Londo cathedral
AL: Giant's Coal - by giant near dark cathedralOn the Giant Blacksmith's corpse in Anor Londo
AL: Golden Ritual Spear - light cathedral, mimic upstairsDrop from a mimic in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Havel's Ring+2 - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Human Dregs - water reservesIn the open in the Water Reserves
AL: Large Soul of a Weary Warrior - left of dark cathedral entranceIn front of the Anor Londo cathedral, slightly to the left
AL: Large Titanite Shard - balcony by dead giantsAfter the Pontiff fight, on the balcony to the right of the area with the Giant Slaves
AL: Large Titanite Shard - bottom of the furthest buttressAt the base of the rightmost flying buttress leading up to Anor Londo
AL: Large Titanite Shard - bottom of the nearest buttressOn the tower leading back from Anor Londo to the shortcut to Irithyll, down the flying buttress closest to the Darkmoon Tomb entrance.
AL: Large Titanite Shard - right after light cathedralAfter Pontiff's cathedral, hugging the wall to the right
AL: Large Titanite Shard - walkway, side path by cathedralAfter the Pontiff fight, going back from the Deacons area to the original cathedral, before a dropdown
AL: Moonlight Arrow - dark cathedral, up right stairsIn the Anor Londo cathedral, up the stairs on the right side
AL: Painting Guardian Gloves - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Gown - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Hood - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Waistcloth - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian's Curved Sword - prison tower raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Proof of a Concord Kept - dark cathedral, up left stairsIn the Anor Londo cathedral, halfway down the stairs on the left side next to some Deacons
AL: Reversal Ring - tomb, chest in cornerIn a chest in Darkmoon Tomb
AL: Ring of Favor - water reserves, both minibossesDropped after killing both of Sulyvahn's Beasts in the Water Reserves
AL: Ring of Favor+1 - light cathedral, upstairsIn the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Silver Mask - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Simple Gem - light cathedral, lizard upstairsDropped by a Crystal Lizard in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Soul of AldrichDropped by Aldrich
AL: Soul of Rosaria - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Soul of a Crestfallen Knight - right of dark cathedral entranceTo the right of the Anor Londo cathedral entrance, past the red-eyed Silver Knight
AL: Soul of a Weary Warrior - plaza, nearerAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Sun Princess Ring - dark cathedral, after bossIn the Anor Londo cathedral after defeating Aldrich, up the elevators in Gwynevere's Chamber
AL: Titanite Scale - top of ladder up to buttressesOn the platform after the stairs leading up to Anor Londo from the Water Reserves building
AL: Twinkling Titanite - lizard after light cathedral #1Dropped a Crystal Lizard straight after the Pontiff fight
AL: Twinkling Titanite - lizard after light cathedral #2Dropped a Crystal Lizard straight after the Pontiff fight
AL: Yorshka's Chime - kill YorshkaDropped by Yorshka upon death.
AP: Ancient Dragon Greatshield - intro, on archwayAfter the Archdragon Peak bonfire, on top of the arch in front of the Ancient Wyvern fight
AP: Calamity Ring - mausoleum, gesture at altarReceived using Path of the Dragon at the Altar by the Mausoleum bonfire
AP: Covetous Gold Serpent Ring+2 - plazaIn the Nameless King boss arena after he is defeated
AP: Dragon Chaser's Ashes - summit, side pathIn the run-up to the Dragon Altar after the Belfry bonfire, in a side path to the left side
AP: Dragon Head Stone - fort, boss dropDropped by Ancient Wyvern
AP: Dragon Tooth - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Dragonslayer Armor - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Gauntlets - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Helm - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Leggings - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Spear - gate after mausoleumIn the gate connecting the Dragon-Kin Mausoleum area to the bridge where the Nameless King fight takes place
AP: Drakeblood Greatsword - mausoleum, NPC dropDropped by the Drakeblood Knight summoned by the Serpent-Man Summoner
AP: Dung Pie - fort, landing after second roomOn a landing going up the stairs from the Ancient Wyvern to the chainaxe Man-Serpent area
AP: Ember - belfry, below bellIn the area below the bell lever, either dropping down near the lever or going down the stairs from the open fountain area after the Belfry bonfire
AP: Ember - fort overlook #1From the right of where Ancient Wyvern first lands
AP: Ember - fort overlook #2From the right of where Ancient Wyvern first lands
AP: Ember - intro, by bonfireNext to the Archdragon Peak bonfire
AP: Great Magic Barrier - drop off belfry roofDropping down to the left from the area with the Havel Knight and the dead Wyvern
AP: Havel's Greatshield - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Havel's Ring+1 - summit, after buildingJust past the building with all of the Man-Serpents on the way to the Dragon Altar, on the left side
AP: Homeward Bone - intro, path to bonfireFrom the start of the area, along the left path leading to the first bonfire
AP: Large Soul of a Crestfallen Knight - summit, by fountainIn the middle of the open fountain area after the Belfry bonfire
AP: Large Soul of a Nameless Soldier - fort, by stairs to first roomto the left of where the Ancient Wyvern lands
AP: Large Soul of a Weary Warrior - fort, centerWhere the Ancient Wyvern lands
AP: Lightning Bolt - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Lightning Clutch Ring - intro, left of boss doorTo the left of gate leading to Ancient Wyvern, past the Rock Lizard
AP: Lightning Gem - intro, side riseFrom the start of the area, up a ledge in between two forked paths toward the first bonfire
AP: Lightning Urn - fort, left of first room entranceOn the path to the left of where the Ancient Wyvern lands, left of the building entrance
AP: Ricard's Rapier - belfry, NPC dropDropped by the Richard Champion summoned by the Serpent-Man Summoner
AP: Ring of Steel Protection - fort overlook, beside stairsTo the right of the area where the Ancient Wyvern lands, dropping down onto the ledge
AP: Soul of a Crestfallen Knight - mausoleum, upstairsFrom the Mausoleum bonfire, up the second set of stairs to the right
AP: Soul of a Nameless Soldier - intro, right before archwayFrom the Archdragon Peak bonfire, going right before the arch before Ancient Wyvern
AP: Soul of a Weary Warrior - intro, first cliff edgeAt the very start of the area on the left cliff edge
AP: Soul of a Weary Warrior - walkway, building windowOn the way to the Belfry bonfire after the sagging wooden bridge, on a ledge visible in a room with a Crystal Lizard, accessible by a tricky jump or just going around the other side
AP: Soul of the Nameless KingDropped by Nameless King
AP: Stalk Dung Pie - fort overlookFrom the right of where Ancient Wyvern first lands
AP: Thunder Stoneplate Ring - walkway, up ladderAfter the long hallway after the Mausoleum bonfire, before the rope bridge, up the long ladder
AP: Titanite Chunk - fort, second room balconyAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left
AP: Titanite Chunk - intro, archway cornerFrom the Archdragon Peak bonfire, under the arch, immediately to the right
AP: Titanite Chunk - intro, behind rockAlmost at the Archdragon Peak bonfire, behind a rock in the area with many Man-Serpents
AP: Titanite Chunk - intro, left before archwayAfter the Archdragon Peak bonfire, going left before the arch before Ancient Wyvern
AP: Titanite Chunk - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Titanite Chunk - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Scale - mausoleum, downstairs balcony #1From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, downstairs balcony #2From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, upstairs balconyFrom the Mausoleum bonfire, up the first stairs to the right, going around toward the Man-Serpent Summoner, on the balcony on the side
AP: Titanite Scale - walkway buildingIn a chest after the sagging wooden bridge on the way to the Belfry, in the building with the Crystal Lizard
AP: Titanite Scale - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Slab - belfry roofNext to the Havel Knight by the dead Wyvern
AP: Titanite Slab - plazaIn the Nameless King boss arena after he is defeated
AP: Twinkling Dragon Torso Stone - summit, gesture at altarReceived using Path of the Dragon at the Altar after the Belfry bonfire. Hawkwood also uses the gesture there when summoned.
AP: Twinkling Titanite - belfry, by ladder to roofIn the chest before the ladder climbing up to the Havel Knight
AP: Twinkling Titanite - fort, down second room balcony ladderAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left and then down the ladder
AP: Twinkling Titanite - fort, end of raftersDropping down to the left of the Mausoleum bonfire, all the way down the wooden rafters
AP: Twinkling Titanite - walkway building, lizardDropped by Crystal Lizard in the building after the sagging wooden bridge toward the Belfry
AP: Twinkling Titanite - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
CA: Coiled Sword - boss dropDropped by Iudex Gundyr
CA: Firebomb - down the cliff edgeAlong the cliff edge before the Iudex Gundyr fight, to the right
CA: Soul of a Deserted Corpse - right of spawnAt the very start of the game
CA: Soul of an Unknown Traveler - by minibossIn the area with the Ravenous Crystal Lizard
CA: Speckled Stoneplate Ring+1 - by minibossIn the area with the Ravenous Crystal Lizard, along the right wall
CA: Titanite Scale - miniboss dropDropped by Ravenous Crystal Lizard
CA: Titanite Shard - jump to coffinMaking a jump to a coffin after the Cemetery of Ash bonfire
CC: Black Blade - tomb, mimicDropped by the mimic before Smouldering Lake
CC: Black Bug Pellet - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Bloodred Moss Clump - atrium lower, down more stairsTo the left before going down the main stairwell in the Catacombs, past the skeleton ambush and where Anri is standing, near the Crystal Lizard
CC: Carthus Bloodring - crypt lower, end of side hallAt the very end of the Bonewheel Skeleton area
CC: Carthus Milkring - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots
CC: Carthus Pyromancy Tome - atrium lower, jump from bridgeDown the hallway to the right before going down the main stairwell in the Catacombs and through an illusory wall on the left, or making a difficult dropdown from the top-level platform
CC: Carthus Rouge - atrium upper, left after entranceTo the right after first entering the Catacombs
CC: Carthus Rouge - crypt across, cornerMaking a difficult jump between the hallway after the first Skeleton Ball and the area at the same level on the opposite side, or going up the stairs from the main hall
CC: Dark Gem - crypt lower, skeleton ball dropDropped by second Skeleton Ball after killing its sorcerer skeleton
CC: Ember - atrium, on long stairwayOn the main stairwell in Catacombs
CC: Ember - crypt lower, shortcut to cavernIn the short hallway with the level shortcut where Knight Slayer Tsorig invades
CC: Ember - crypt upper, end of hall past holeGoing right from the Catacombs bonfire, down the hall to the left, then to the right. After a hole that drops down into the Bonewheel Skeleton area.
CC: Fire Gem - cavern, lizardDropped by a Crystal Lizard found between the Catacombs main halls and the ledge overlooking the bridge you can cut down
CC: Grave Warden Pyromancy Tome - boss arenaIn Wolnir's arena, or in the back left of the room containing his bonfire if not picked up in the arena
CC: Grave Warden's Ashes - crypt across, cornerFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, on the far left side. Stairwell past the illusory wall is most direct.
CC: Homeward Bone - Irithyll bridgeFound right before the wall blocking access to Irithyll
CC: Large Soul of a Nameless Soldier - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Large Soul of a Nameless Soldier - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Large Soul of an Unknown Traveler - crypt upper, hall middleGoing right from the Catacombs bonfire, then down the long hallway after the hallway to the left
CC: Large Titanite Shard - crypt across, middle hallFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, in a middle hallway
CC: Large Titanite Shard - crypt upper, skeleton ball hallGoing right from the Catacombs bonfire, to the end of the hallway where second Skeleton Ball rolls
CC: Large Titanite Shard - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Old Sage's Blindfold - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Pontiff's Right Eye - Irithyll bridge, miniboss dropDropped by killing Sulyvahn's Beast on the bridge to Irithyll or in the lake below
CC: Ring of Steel Protection+2 - atrium upper, drop onto pillarFrom the first bridge in Catacombs where the first skeletons are encountered, parallel to the long stairwell, walk off onto a pillar on the left side.
CC: Sharp Gem - atrium lower, right before exitDown the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of High Lord WolnirDropped by High Lord Wolnir
CC: Soul of a Demon - tomb, miniboss dropDropped by the Fire Demon before Smouldering Lake
CC: Soul of a Nameless Soldier - atrium lower, down hallAll the way down the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of a Nameless Soldier - atrium upper, up more stairsFrom the room before the Catacombs main stairwell, up the two ramps and to the end of the long hallway crossing the room
CC: Thunder Stoneplate Ring+1 - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots, behind one of the pillars
CC: Titanite Shard - atrium lower, corner by stairsTo the left before going down the main stairwell in the Catacombs, behind the pensive Carthus Cursed Sword Skeleton
CC: Titanite Shard - crypt lower, left of entranceIn the main hall after the Catacombs bonfire, down the stairs and to the left
CC: Titanite Shard - crypt lower, start of side hallIn the Bonewheel Skeleton area, on the left side under a Writhing Flesh
CC: Twinkling Titanite - atrium lower, lizard down more stairsDropped by a Crystal Lizard found to the left before going down the main stairwell in the Catacombs, past the skeleton ambush and past where Anri is standing
CC: Undead Bone Shard - crypt upper, skeleton ball dropDropped by first Skeleton Ball after killing its sorcerer skeleton
CC: Witch's Ring - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Yellow Bug Pellet - cavern, on overlookTo the right of the Carthus Curved Sword Skeleton overlooking the pit Horace falls into
CD: Aldrich's Sapphire - side chapel, miniboss dropDropped by the Deep Accursed
CD: Arbalest - upper roofs, end of furthest buttressBefore the rafters on the way to Rosaria, up a flying buttress, past a halberd-wielding Large Hollow Soldier to the right, and down another flying buttress to the right
CD: Archdeacon Holy Garb - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon Skirt - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon White Crown - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Armor of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Astora Greatsword - graveyard, left of entranceDown one of the side paths to the left in the Reanimated Corpse area
CD: Barbed Straight Sword - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Black Eye Orb - Rosaria from Leonhard's questOn Rosaria's corpse after joining Rosaria's Fingers, exhausting Leonhard's dialogue there and reaching the Profaned Capital bonfire.
CD: Blessed Gem - upper roofs, raftersIn the rafters leading to Rosaria, guarded by a Cathedral Knight to the right
CD: Crest Shield - path, drop down by Cathedral of the Deep bonfireOn a grave near the Cathedral of the Deep bonfire, accessed by dropping down to the right
CD: Curse Ward Greatshield - by ladder from white tree to moatTaking a right after the Infested Corpse graveyard, before the shortcut ladder down to the Ravenous Crystal Lizard area
CD: Deep Braille Divine Tome - mimic by side chapelDropped by the Mimic before the room with the patrolling Cathedral Knight and Deep Accursed
CD: Deep Gem - down stairs by first elevatorComing from the room where you first see deacons, go down instead of continuing to the main cathedral room. Guarded by a pensive Cathedral Evangelist.
CD: Deep Ring - upper roofs, passive mob drop in first towerDropped by the passive Deacon on the way to Rosaria
CD: Drang Armor - main hall, eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Gauntlets - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Hammers - main hall eastIn the Giant Slave muck pit leading up to Deacons, underneath the stairwell
CD: Drang Shoes - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Duel Charm - by first elevatorAfter opening the cathedral's backdoor, where the Deacon enemies are first seen, under a fountain that spouts poison
CD: Duel Charm - next to Patches in onion armorTo the right of the bridge leading to Rosaria, from the Deacons side. Patches will lower the bridge if you try to cross from this side.
CD: Ember - PatchesSold by Patches in Firelink Shrine
CD: Ember - by back doorPast the pair of Grave Wardens and the Cathedral backdoor against a wall, guarded by a greataxe-wielding Large Hollow Soldier
CD: Ember - edge of platform before bossOn the edge of the chapel before Deacons overlooking the Giant Slaves
CD: Ember - side chapel upstairs, up ladderUp a ladder and past the Cathedral Evangelist from the top level of the room with the patrolling Cathedral Knight and Deep Accursed
CD: Ember - side chapel, miniboss roomIn the room with the Deep Accursed
CD: Estus Shard - monument outside Cleansing ChapelRight outside of the Cleansing Chapel. Requires killing praying hollows.
CD: Executioner's Greatsword - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area
CD: Exploding Bolt - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Fading Soul - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area, next to the Executioner's Greatsword
CD: Gauntlets of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Helm of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Herald Armor - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Gloves - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Helm - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Trousers - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Heysel Pick - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Homeward Bone - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Horsehoof Ring - PatchesSold or dropped by Patches after he mentions Greirat
CD: Large Soul of an Unknown Traveler - by white tree #1In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - by white tree #2In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - lower roofs, semicircle balconyOn the cathedral roof after climbing up the flying buttresses, on the edge of the semicircle platform balcony
CD: Large Soul of an Unknown Traveler - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Large Soul of an Unknown Traveler - main hall south, side pathDown a side path with poison-spouting fountains in the main cathedral room, accessible from the Cleansing Chapel shortcut, patrolled by a Cathedral Knight
CD: Large Soul of an Unknown Traveler - path, against outer wallFrom the Cathedral of the Deep bonfire after the Brigand, against the wall in the area with the dogs and crossbowmen
CD: Leggings of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Lloyd's Sword Ring - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Maiden Gloves - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Hood - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Robe - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Skirt - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Notched Whip - Cleansing ChapelIn a corner of the Cleansing Chapel
CD: Paladin's Ashes - path, guarded by lower NPCAt the very start of the area, guarded by the Fallen Knight
CD: Pale Tongue - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Pale Tongue - upper roofs, outdoors far endBefore the rafters on the way to Rosaria, up a flying buttress and straight right, passing a halberd-wielding Large Hollow Soldier
CD: Poisonbite Ring - moat, hall past minibossIn the pit with the Infested Corpse, accessible from the Ravenous Crystal Lizard area or from dropping down near the second Cleansing Chapel shortcut
CD: Red Bug Pellet - lower roofs, up stairs between buttressesIn the area after the cathedral roof against the wall of the cathedral, down the path from the Cathedral Evangelist.
CD: Red Bug Pellet - right of cathedral front doorsUp the stairs past the Infested Corpse graveyard and the left, toward the roof path to the right of the cathedral doors
CD: Red Sign Soapstone - passive mob drop by Rosaria's Bed ChamberDropped by passive Corpse-grub against the wall near the entrance to Rosaria's Bed Chamber
CD: Repair Powder - by white treeIn the graveyard with the White Birch and Infested Corpses
CD: Ring of Favor+2 - upper roofs, on buttressBefore the rafters on the way to Rosaria, up a flying buttress, behind a greataxe-wielding Large Hollow Soldier to the left
CD: Ring of the Evil Eye+1 - by stairs to bossBefore the stairs leading down into the Deacons fight
CD: Rosaria's Fingers - RosariaGiven by Rosaria.
CD: Rusted Coin - don't forgive PatchesGiven by Patches after not forgiving him after he lowers the bridge in Cathedral of the Deep.
CD: Rusted Coin - left of cathedral front doors, behind cratesUp the stairs past the Infested Corpse graveyard and to the left, hidden behind some crates to the left of the cathedral door
CD: Saint Bident - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Saint-tree Bellvine - moat, by waterIn the Infested Corpse moat beneath the Cathedral
CD: Seek Guidance - side chapel upstairsAbove the room with the patrolling Cathedral Knight and Deep Accursed, below a writhing flesh on the ceiling.
CD: Shotel - PatchesSold by Patches
CD: Small Doll - boss dropDropped by Deacons of the Deep
CD: Soul of a Nameless Soldier - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Soul of a Nameless Soldier - lower roofs, side roomComing from the cathedral roof, past the three crossbowmen to the path patrolled by the halberd-wielding Large Hollow Soldier, in a room to the left with many thralls.
CD: Soul of a Nameless Soldier - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Soul of the Deacons of the DeepDropped by Deacons of the Deep
CD: Spider Shield - NPC drop on pathDropped by the brigand at the start of Cathedral of the Deep
CD: Spiked Shield - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Titanite Scale - moat, miniboss dropDropped by the Ravenous Crystal Lizard outside of the Cathedral
CD: Titanite Shard - Cleansing Chapel windowsill, by minibossOn the ledge dropping back down into Cleansing Chapel from the area with the Ravenous Crystal Lizard
CD: Titanite Shard - moat, far endBehind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Titanite Shard - moat, up a slopeUp one of the slopes in the Ravenous Crystal Lizard area
CD: Titanite Shard - outside building by white treePast the Infested Corpse graveyard to the left, hidden along the left wall of the building with the shortcut ladder and Curse Ward Greatshield
CD: Titanite Shard - path, side path by Cathedral of the Deep bonfireUp a path to the left after the Cathedral of the Deep bonfire, after the Fallen Knight and before the Brigand
CD: Twinkling Titanite - moat, lizard #1Dropped by the Crystal Lizard behind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - moat, lizard #2Dropped by the Crystal Lizard under the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - path, lizard #1Dropped by the first Crystal Lizard after the Crystal Sage fight
CD: Twinkling Titanite - path, lizard #2Dropped by the second Crystal Lizard after the Crystal Sage fight
CD: Undead Bone Shard - gravestone by white treeIn the graveyard with the Infested Corpses, on a coffin partly hanging off of the ledge
CD: Undead Hunter Charm - lower roofs, up stairs between buttressesIn the area after the cathedral roof guarded by a Cathedral Evangelist. Can be jumped to from a flying buttress or by going around and back
CD: Winged Spear - kill PatchesDropped by Patches when killed in his own armor.
CD: Xanthous Crown - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Young White Branch - by white tree #1By the White Birch tree in the Infested Corpse graveyard
CD: Young White Branch - by white tree #2By the White Birch tree in the Infested Corpse graveyard
CKG: Black Firebomb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Claw - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Dark Gem - under lone stairwayFollowing the left wall, behind the standalone set of stairs
CKG: Dragonscale Ring - shortcut, leave halfway down liftFrom the middle level of the second elevator, toward the Oceiros boss fight
CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Estus Shard - balconyFrom the middle level of the first Consumed King's Gardens elevator, out the balcony and to the right
CKG: Human Pine Resin - pool by liftOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, in a toxic pool
CKG: Human Pine Resin - toxic pool, past rotundaIn between two platforms near the middle of the garden, by a tree in a toxic pool
CKG: Magic Stoneplate Ring - mob drop before bossDropped by the Cathedral Knight closest to the Oceiros fog gate
CKG: Ring of Sacrifice - under balconyAlong the right wall of the garden, next to the first elevator building
CKG: Sage Ring+2 - balcony, drop onto rubble, jump backFrom the middle platform of the first elevator in the target, going out and dropping off to the left, and then running off onto the ruined arch behind.
CKG: Shadow Garb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Gauntlets - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Leggings - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Mask - under center platformUnder the platform in the middle of the garden, in the toxic pool
CKG: Soul of Consumed OceirosDropped by Consumed King Oceiros
CKG: Soul of a Weary Warrior - before first liftOn the path leading to the first elevator from Lothric Castle
CKG: Titanite Chunk - balcony, drop onto rubbleFrom the middle platform of the first elevator, dropping down to the left
CKG: Titanite Chunk - right of shortcut lift bottomOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, all the way to the end
CKG: Titanite Chunk - shortcutRight inside of the shortcut door leading to Oceiros from Lothric/Dancer bonfire
CKG: Titanite Chunk - up lone stairwayFollowing the left wall of the garden, in and up the standalone set of stairs
CKG: Titanite Scale - shortcutIn the room leading to the Oceiros shortcut elevator from Lothric/Dancer, in the first floor alcove.
CKG: Titanite Scale - tomb, chest #1Chest after Oceiros fight
CKG: Titanite Scale - tomb, chest #2Chest after Oceiros fight
CKG: Wood Grain Ring+1 - by first elevator bottomBehind the first elevator going down into the garden, in the toxic pool
DH: Aquamarine Dagger - castle, up stairsUp the second flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Black Firebomb - ruins, up windmill from bonfireTo the left of the Earthen Peak Ruins bonfire, past the ruined windmill, next to many Poisonhorn bugs.
DH: Covetous Silver Serpent Ring+3 - pantry upstairs, drop downAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then dropping down after exiting the building from the last room.
DH: Desert Pyromancer Garb - ruins, by shack near cliffBehind a shack near the edge of the cliff of the area targeted by the second angel.
DH: Desert Pyromancer Gloves - swamp, far rightAfter dropping down in the poison swamp area, against the wall straight to the right.
DH: Desert Pyromancer Hood - swamp upper, tunnel endAt the end of the tunnel with Desert Pyromancy Zoey, to the right of the final branches.
DH: Desert Pyromancer Skirt - swamp right, by rootsIn the poison swamp, against a tree guarded by a few Poisonhorn bugs in the front right.
DH: Divine Blessing - library, after dropAfter the dropdown where an angel first targets you, behind you
DH: Divine Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Divine Blessing - swamp upper, building roofOn a rooftop of one of the buildings bordering the poison swamp. Can be reached by dropping down from the final tree branch and accessing the roof to the right.
DH: Ember - castle, behind spireAt the start of the area, behind a spire to the right of first drop down
DH: Ember - pantry, behind crates just before upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room end of the building to the right, up stairs past an illusory wall to the left, in the second-to-last room of the sequence, behind some crates to the left.
DH: Ember - ruins, alcove before swampIn an alcove providing cover from the second angel's projectiles, before dropping down in the poison swamp area.
DH: Ember - ruins, alcove on cliffIn the area with the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, dropping down to the right of the bonfire.
DH: Ember - shopSold by Stone-humped Hag, or in her ashes
DH: Flame Fan - swamp upper, NPC dropDropped by Desert Pyromancer Zoey
DH: Giant Door Shield - ruins, path below far shackDescending down a path from the edge of the cliff of the area targeted by the second angel, to the very end of the cliff.
DH: Great Soul Dregs - pantry upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then all the way to the end of the last room.
DH: Harald Curved Greatsword - swamp left, under rootIn the back leftmost area of the poison swamp, underneath the tree branch leading up and out, guarded by a stationary Harald Legion Knight.
DH: Hidden Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Homeward Bone - end of path from churchImmediately before dropping into the area with the Earthen Peak Ruins bonfire, next to Gael's flag.
DH: Homeward Bone - swamp left, on rootAll the way to the end of a short path in the back leftmost area of the poison swamp, where you can plunge attack the stationary Harald Legion Knight.
DH: Large Soul of a Weary Warrior - parapets, hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, in a corner to the left.
DH: Large Soul of a Weary Warrior - swamp centerIn the middle of the poison swamp.
DH: Large Soul of a Weary Warrior - swamp, under overhangIn the cavern adjacent to the poison swamp, surrounded by a few Poisonhorn bugs.
DH: Lightning Urn - wall outside churchAfter the dropdown where an angel first targets you, against the wall on the left.
DH: Loincloth - swamp, left edgeIn the leftmost edge of the poison swamp after dropping down, guarded by 6 Poisonhorn bugs.
DH: Lothric War Banner - parapets, end of hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, at the end of the hallway to the right.
DH: Murky Hand Scythe - library, behind bookshelvesAfter the first long drop into the building which looks like Grand Archives, to the left up the bookshelf stairs and behind the bookshelves
DH: Murky Longstaff - pantry, last roomAfter exiting the building with the Lothric Knights where the front crumbles, in the third-furthest room in the building to the right.
DH: Prism Stone - swamp upper, tunnel startNear the start of the tunnel with Desert Pyromancer Zoey.
DH: Projected Heal - parapets balconyAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman, against a wall in the area with the Lothric War Banner Knight and many murkmen.
DH: Purple Moss Clump - swamp shackIn the ruined shack with Poisonhorn bugs straight ahead of the dropdown into the poison swamp area.
DH: Ring of Favor+3 - swamp right, up rootUp the long branch close to the dropdown into the poison swamp area, in front of the cavern.
DH: Ring of Steel Protection+3 - ledge before churchAfter the dropdown where an angel first targets you, on an exposed edge to the left. Difficult to get without killing the angel.
DH: Rusted Coin - behind fountain after churchAfter exiting the building with the Lothric Knights where the front crumbles, behind the fountain on the right side.
DH: Rusted Gold Coin - shopSold by Stone-humped Hag, or in her ashes
DH: Siegbräu - LappGiven by Lapp after collecting the Titanite Slab in Earthen Peak Ruins, or left after Demon Princes fight, or dropped upon death if not given.
DH: Small Envoy Banner - boss dropFound in the small room after beating Demon Prince.
DH: Soul of a Crestfallen Knight - church, altarIn the building where the front crumbles, guarded by the two Lothric Knights at the front of the chapel.
DH: Soul of a Weary Warrior - castle overhangThe bait item at the start of the area which falls down with you into the ruined building below.
DH: Soul of the Demon PrinceDropped by Demon Prince
DH: Splitleaf Greatsword - shopSold by Stone-humped Hag, or in her ashes
DH: Titanite Chunk - castle, up stairsUp first flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Titanite Chunk - pantry, first roomAfter exiting the building with the Lothric Knights where the front crumbles, on a ledge in the first room of the building to the right.
DH: Titanite Chunk - path from church, by pillarBefore dropping into the area with the Earthen Peak Ruins bonfire, behind a pillar in front of a murkman pool.
DH: Titanite Chunk - ruins, by far shackIn front of a shack at the far edge of the cliff of the area targeted by the second angel. There is a shortcut dropdown to the left of the building.
DH: Titanite Chunk - ruins, path from bonfireAt the Earthen Peak Ruins bonfire, straight a bit then all the way left, near the edge of the cliff in the area targeted by the second angel.
DH: Titanite Chunk - swamp right, drop partway up rootPartway up the long branch close to the dropdown into the poison swamp area, in front of the cavern, dropping down to a branch to the left.
DH: Titanite Chunk - swamp, along buildingsAfter dropping down into the poison swamp, along the buildings on the left side.
DH: Titanite Chunk - swamp, path to upperPartway up the branch that leads out of the poison swamp, on a very exposed branch jutting out to the left.
DH: Titanite Scale - library, back of roomAfter the first long drop into the building which looks like Grand Archives, behind you at the back of the room
DH: Titanite Scale - swamp upper, drop and jump into towerAt the very end of the last tree branch before dropping down toward the Within Earthen Peak Ruins bonfire, drop down to the left instead. Make a jump into the interior of the overturned tower to the left.
DH: Titanite Slab - swamp, path under overhangDeep within the cavern adjacent to the poison swamp, to the back and then left. Alternatively, given by Lapp after exhausting dialogue near the bonfire and dying, or left after he moves on, or dropped upon death if not given.
DH: Twinkling Titanite - library, chandelierAfter the first long drop into the building which looks like Grand Archives, straight ahead hanging from a chandelier on the ground
DH: Twinkling Titanite - path after church, mob dropDropped the pilgrim responsible for the first angel encountered, below the spire bridge that forms by crashing into the building.
DH: Twinkling Titanite - ruins, alcove on cliff, mob dropDropped by the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, or dropping down to the right of the bonfire.
DH: Twinkling Titanite - ruins, root near bonfireTreasure visible straight ahead of the Earthen Peak Ruins bonfire on a branch. Can be accessed by following the right wall from the bonfire until a point of access onto the branch is found.
DH: Twinkling Titanite - swamp upper, drop onto rootOn the final tree branches before dropping down toward the Within Earthen Peak Ruins bonfire, drop down on a smaller branch to the right. This loops back to the original branch.
DH: Twinkling Titanite - swamp upper, mob drop on roofDropped by the pilgrim responsible for the third angel in the swamp. Rather than heading left into the tunnel with Desert Pyromancy Zoey, go right onto a shack roof. Drop down onto a tree branch at the end, then drop down to another roof.
FK: Antiquated Dress - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Gloves - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Skirt - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Atonement - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Black Bow of Pharis - miniboss drop, by keep ruins near wallDropped the Elder Ghru on the left side of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Black Bug Pellet - perimeter, hill by boss doorOn the small hill to the right of the Abyss Watchers entrance, guarded by a spear-wielding Ghru Grunt
FK: Cinders of a Lord - Abyss WatcherDropped by Abyss Watchers
FK: Crown of Dusk - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Dark Stoneplate Ring+2 - keep ruins ritual island, behind wallHidden behind the right wall of the ritual fire before Keep Ruins
FK: Dragon Crest Shield - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Dreamchaser's Ashes - keep proper, illusory wallNear the Old Wolf of Farron bonfire, behind an illusory wall near the Crystal Lizard
FK: Ember - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Ember - perimeter, path to bossGuarded by a spear-wielding Ghru Grunt to the right of the main path leading up to Abyss Watchers
FK: Ember - upper keep, by miniboss #1Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Ember - upper keep, by miniboss #2Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Estus Shard - between Farron Keep bonfire and left islandStraight ahead from the Farron Keep bonfire to the ritual fire stairs, guarded by a slug
FK: Gold Pine Bundle - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Golden Scroll - hidden caveIn a cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Great Magic Weapon - perimeter, by door to Road of SacrificesNext to the shortcut leading from Farron Keep Perimeter back into Crucifixion Woods, past the Ravenous Crystal Lizard
FK: Greataxe - upper keep, by minibossGuarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Greatsword - ramp by keep ruins ritual islandIn the middle of the swamp, on the pair of long ramps furthest from the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Havel's Armor - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Helm - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Leggings - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Heavy Gem - upper keep, lizard on stairsDropped by the Crystal Lizard that scurries up the stairs in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Hollow Gem - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Homeward Bone - right island, behind fireBehind the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Iron Flesh - Farron Keep bonfire, right after exitIn the open in the swamp, heading straight right from Farron Keep bonfire
FK: Large Soul of a Nameless Soldier - corner of keep and right islandHidden in a corner to the right of the stairs leading up to the ritual fire from the basilisk area
FK: Large Soul of a Nameless Soldier - near wall by right islandTo the left of the stairs leading up to the ritual fire from the Basilisk area, by the keep wall
FK: Large Soul of an Unknown Traveler - by white treeOn a tree close to the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Large Titanite Shard - upper keep, lizard by wyvernDropped by the farther Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Large Titanite Shard - upper keep, lizard in openDropped by the closer Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Lightning Spear - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Lingering Dragoncrest Ring - by white tree, miniboss dropDropped by the Greater Crab patrolling the birch tree where the Giant shoots arrows
FK: Magic Stoneplate Ring+1 - between right island and wallBehind a tree in the basilisk area, heading directly right from Farron Keep bonfire
FK: Manikin Claws - Londor Pale Shade dropDropped by Londor Pale Shade when he invades near the basilisks, if Yoel or Yuria have been betrayed
FK: Nameless Knight Armor - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Gauntlets - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Helm - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Leggings - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Pharis's Hat - miniboss drop, by keep ruins near wallDropped the Elder Ghru in the back of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Poison Gem - near wall by keep ruins bridgeFrom the left of the bridge leading from the ritual fire to the Keep Ruins bonfire, guarded by the three Elder Ghru
FK: Prism Stone - by left island stairsOn an island to the left of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Purple Moss Clump - Farron Keep bonfire, around right cornerAlong the inner wall of the keep, making an immediate right from Farron Keep bonfire
FK: Purple Moss Clump - keep ruins, ritual islandClose to the ritual fire before the Keep Ruins bonfire
FK: Purple Moss Clump - ramp directly in front of Farron Keep bonfireIn the middle of the swamp, on the pair of long ramps closest to the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Ragged Mask - Farron Keep bonfire, around left cornerAlong the inner wall of the keep, making an immediate left from Farron Keep bonfire, guarded by slugs
FK: Repair Powder - outside hidden caveAlong the keep wall in the basilisk area, outside of the cave with the Elizabeth corpse and Golden Scroll
FK: Rotten Pine Resin - left island, behind fireIn the area behind the ritual fire which is straight ahead of the Farron Keep bonfire
FK: Rotten Pine Resin - outside pavilion by left islandFrom the Farron Keep bonfire straight ahead to the pavilion guarded by the Darkwraith, just to the left of the ritual fire stairs
FK: Rusted Gold Coin - right island, behind wallHidden behind the right wall of the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Sage's Coal - pavilion by left islandIn the pavilion guarded by a Darkwraith, straight ahead from the Farron Keep bonfire to the left of the ritual fire stairs
FK: Sage's Scroll - near wall by keep ruins bonfire islandAlong the keep inner wall, heading left from the stone doors past the crab area, surrounded by many Ghru enemies
FK: Shriving Stone - perimeter, just past stone doorsPast the stone doors, on the path leading up to Abyss Watchers by the Corvians
FK: Soul of a Nameless Soldier - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Soul of a Stray Demon - upper keep, miniboss dropDropped by Stray Demon on the bridge above Farron Keep
FK: Soul of the Blood of the WolfDropped by Abyss Watchers
FK: Stone Parma - near wall by left islandAlong the inner wall of the keep, making a left from Farron Keep bonfire but before the area with the Darkwraith, guarded by a slug
FK: Sunlight Talisman - estus soup island, by ladder to keep properBy the pot of estus soup to the left of the stairs leading up to Old Wolf of Farron
FK: Titanite Scale - perimeter, miniboss dropDropped by Ravenous Crystal Lizard near the shortcut from Farron Keep back to Road of Sacrifices
FK: Titanite Shard - Farron Keep bonfire, left after exitAlong the inner wall of the keep, making a left from Farron Keep bonfire, by the second group of four slugs
FK: Titanite Shard - between left island and keep ruinsIn the swamp area with the Ghru Leaper between the Keep Ruins ritual fire and ritual fire straight ahead of Farron Keep bonfire, opposite from the keep wall
FK: Titanite Shard - by keep ruins ritual island stairsBy the stairs leading up to the Keep Ruins ritual fire from the middle of the swamp
FK: Titanite Shard - by ladder to keep properIn the swamp area close to the foot of the ladder leading to Old Wolf of Farron bonfire
FK: Titanite Shard - by left island stairsIn front of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Titanite Shard - keep ruins bonfire island, under rampUnder the ramp leading down from the Keep Ruins bonfire
FK: Titanite Shard - swamp by right islandBehind a tree patrolled by an Elder Ghru close to the ritual fire stairs
FK: Twinkling Dragon Head Stone - Hawkwood dropDropped by Hawkwood after killing him in the Abyss Watchers arena, after running up to the altar in Archdragon Peak. Twinkling Dragon Torso Stone needs to be acquired first.
FK: Twinkling Titanite - keep proper, lizardDropped by the Crystal Lizard on the balcony behind the Old Wolf of Farron bonfire
FK: Undead Bone Shard - pavilion by keep ruins bonfire islandIn a standalone pavilion down the ramp from Keep Ruins bonfire and to the right
FK: Watchdogs of Farron - Old WolfGiven by Old Wolf of Farron.
FK: Wolf Ring+1 - keep ruins bonfire island, outside buildingTo the right of the building with the Keep Ruins bonfire, when approached from the ritual fire
FK: Wolf's Blood Swordgrass - by ladder to keep properTo the left of the ladder leading up to the Old Wolf of Farron bonfire
FK: Young White Branch - by white tree #1Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Young White Branch - by white tree #2Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FS: Acid Surge - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Affinity - KarlaSold by Karla after recruiting her, or in her ashes
FS: Alluring Skull - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Arstor's Spear - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Aural Decoy - OrbeckSold by Orbeck
FS: Billed Mask - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Dress - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Fire Orb - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Flame - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Gauntlets - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Hand Armor - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
FS: Black Hand Hat - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
FS: Black Iron Armor - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Gauntlets - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Helm - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Leggings - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Leggings - shop after killing YuriaDropped by Yuria upon death or quest completion.
FS: Black Serpent - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Blessed Weapon - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Blue Tearstone Ring - GreiratGiven by Greirat upon rescuing him from the High Wall cell
FS: Boulder Heave - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Bountiful Light - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Bountiful Sunlight - Ludleth for RosariaBoss weapon for Rosaria, available after Leonhard is killed
FS: Broken Straight Sword - gravestone after bossNear the grave after Iudex Gundyr fight
FS: Budding Green Blossom - shop after killing Creighton and AL bossSold by Handmaid after receiving Silvercat Ring item lot from Sirris and defeating Aldrich
FS: Bursting Fireball - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Caressing Tears - IrinaSold by Irina after recruiting her, or in her ashes
FS: Carthus Beacon - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Carthus Flame Arc - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Cast Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Chaos Bed Vestiges - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Chaos Storm - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Clandestine Coat - shop with Orbeck's AshesSold by Handmaid after giving Orbeck's Ashes and reloading
FS: Cleric's Candlestick - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Cracked Red Eye Orb - LeonhardGiven by Ringfinger Leonhard in Firelink Shrine after reaching Tower on the Wall bonfire
FS: Crystal Hail - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Magic Weapon - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Crystal Sage's Rapier - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Soul Spear - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Dancer's Armor - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Crown - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Enchanted Swords - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Dancer's Gauntlets - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Leggings - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dark Blade - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Dark Edge - KarlaSold by Karla after recruiting her, or in her ashes
FS: Dark Hand - Yuria shopSold by Yuria
FS: Darkdrift - kill YuriaDropped by Yuria upon death or quest completion.
FS: Darkmoon Longbow - Ludleth for AldrichBoss weapon for Aldrich
FS: Dead Again - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Deep Protection - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Deep Soul - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Demon's Fist - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Greataxe - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Scar - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Divine Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Divine Blessing - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Dragonscale Armor - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonscale Waistcloth - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonslayer Greataxe - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Greatshield - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Swordspear - Ludleth for NamelessBoss weapon for Nameless King
FS: Dried Finger - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: East-West Shield - tree by shrine entranceIn a tree to the left of the Firelink Shrine entrance
FS: Eastern Armor - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Gauntlets - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Helm - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Leggings - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Elite Knight Armor - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Gauntlets - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Helm - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Leggings - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Ember - Dragon Chaser's AshesSold by Handmaid after giving Dragon Chaser's Ashes
FS: Ember - Grave Warden's AshesSold by Handmaid after giving Grave Warden's Ashes
FS: Ember - GreiratSold by Greirat after recruiting him, or in his ashes
FS: Ember - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Ember - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Ember - above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
FS: Ember - path right of Firelink entranceOn a cliffside to the right of the main path leading up to Firelink Shrine, guarded by a dog
FS: Ember - shopSold by Handmaid
FS: Ember - shop for Greirat's AshesSold by Handmaid after Greirat pillages Lothric Castle and handing in ashes
FS: Embraced Armor of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Executioner Armor - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Gauntlets - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Helm - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Leggings - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Exile Armor - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Gauntlets - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Leggings - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Mask - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Faraam Armor - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Faraam Boots - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Faraam Gauntlets - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Faraam Helm - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Farron Dart - OrbeckSold by Orbeck
FS: Farron Dart - shopSold by Handmaid
FS: Farron Flashsword - OrbeckSold by Orbeck
FS: Farron Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Farron Hail - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Farron Ring - HawkwoodGiven by Hawkwood, or dropped upon death, after defeating Abyss Watchers.
FS: Fire Orb - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Fire Surge - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Fire Whip - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Fireball - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Firelink Armor - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Gauntlets - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Greatsword - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Firelink Helm - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Leggings - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firestorm - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Flash Sweat - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Force - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Frayed Blade - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Friede's Great Scythe - Ludleth for FriedeBoss weapon for Sister Friede
FS: Gael's Greatsword - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Gauntlets of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Gnaw - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Golden Bracelets - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Golden Crown - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Grave Key - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Great Chaos Fire Orb - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Great Combustion - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Great Farron Dart - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Great Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Great Soul Arrow - OrbeckSold by Orbeck
FS: Greatsword of Judgment - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Gundyr's Armor - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Gauntlets - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Halberd - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Gundyr's Helm - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Leggings - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Havel's Ring - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Hawkwood's Shield - gravestone after Hawkwood leavesLeft by Hawkwood after defeating Abyss Watchers, Curse-Rotted Greatwood, Deacons of the Deep, and Crystal Sage
FS: Hawkwood's Swordgrass - Andre after gesture in AP summitGiven by Andre after praying at the Dragon Altar in Archdragon Peak, after acquiring Twinkling Dragon Torso Stone.
FS: Heal - IrinaSold by Irina after recruiting her, or in her ashes
FS: Heal Aid - shopSold by Handmaid
FS: Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Heavy Soul Arrow - Yoel/Yuria shopSold by Yoel/Yuria
FS: Helm of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Hidden Blessing - Dreamchaser's AshesSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Patches after searching GASold by Handmaid after giving Dreamchaser's Ashes, saying where they were found
FS: Hidden Body - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hidden Weapon - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hollowslayer Greatsword - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Homeward - IrinaSold by Irina after recruiting her, or in her ashes
FS: Homeward Bone - cliff edge after bossAlong the cliff edge straight ahead of the Iudex Gundyr fight
FS: Homeward Bone - path above shrine entranceTo the right of the Firelink Shrine entrance, up a slope and before the ledge on top of a coffin
FS: Homing Crystal Soulmass - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Homing Soulmass - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Karla's Coat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Coat - kill KarlaDropped from Karla upon death
FS: Karla's Gloves - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Gloves - kill KarlaDropped from Karla upon death
FS: Karla's Pointed Hat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Pointed Hat - kill KarlaDropped from Karla upon death
FS: Karla's Trousers - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Trousers - kill KarlaDropped from Karla upon death
FS: Leggings of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Leonhard's Garb - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Gauntlets - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Trousers - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Life Ring - Dreamchaser's AshesSold by Handmaid after giving Dreamchaser's Ashes
FS: Lifehunt Scythe - Ludleth for AldrichBoss weapon for Aldrich
FS: Lift Chamber Key - LeonhardGiven by Ringfinger Leonhard after acquiring a Pale Tongue.
FS: Lightning Storm - Ludleth for NamelessBoss weapon for Nameless King
FS: Lloyd's Shield Ring - Paladin's AshesSold by Handmaid after giving Paladin's Ashes
FS: Londor Braille Divine Tome - Yuria shopSold by Yuria
FS: Lorian's Armor - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Gauntlets - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Greatsword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Lorian's Helm - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Leggings - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lothric's Holy Sword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Magic Barrier - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Magic Shield - OrbeckSold by Orbeck
FS: Magic Shield - Yoel/Yuria shopSold by Yoel/Yuria
FS: Magic Weapon - OrbeckSold by Orbeck
FS: Magic Weapon - Yoel/Yuria shopSold by Yoel/Yuria
FS: Mail Breaker - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Master's Attire - NPC dropDropped by Sword Master
FS: Master's Gloves - NPC dropDropped by Sword Master
FS: Med Heal - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Millwood Knight Armor - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Gauntlets - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Helm - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Leggings - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Moaning Shield - EygonDropped by Eygon of Carim
FS: Moonlight Greatsword - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: Morion Blade - Yuria for Orbeck's AshesGiven by Yuria after giving Orbeck's Ashes after she asks you to assassinate him, after he moves to Firelink Shrine. Can be done without killing Orbeck, by completing his questline.
FS: Morne's Armor - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Gauntlets - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Great Hammer - EygonDropped by Eygon of Carim
FS: Morne's Helm - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Leggings - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Old King's Great Hammer - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Old Moonlight - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Ordained Dress - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Hood - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Trousers - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Pale Shade Gloves - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Robe - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Trousers - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pestilent Mist - Orbeck for any scrollSold by Orbeck after giving him any scroll
FS: Poison Mist - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Pontiff's Left Eye - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Prisoner's Chain - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Profaned Greatsword - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Profuse Sweat - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Rapport - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Refined Gem - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Repair - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Repeating Crossbow - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Replenishment - IrinaSold by Irina after recruiting her, or in her ashes
FS: Ring of Sacrifice - Yuria shopSold by Yuria, or by Handmaid after giving Hollow's Ashes
FS: Rose of Ariandel - Ludleth for FriedeBoss weapon for Sister Friede
FS: Rusted Gold Coin - don't forgive PatchesGiven by Patches after not forgiving him after he locks you in the Bell Tower.
FS: Sage's Big Hat - shop after killing RS bossSold by Handmaid after defeating Crystal Sage
FS: Saint's Ring - IrinaSold by Irina after recruiting her, or in her ashes
FS: Seething Chaos - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Silvercat Ring - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Skull Ring - kill LudlethDropped by Ludleth upon death, including after placing all cinders. Note that if killed before giving Transposing Kiln, transposition is not possible.
FS: Slumbering Dragoncrest Ring - Orbeck for buying four specific spellsGiven by Orbeck after purchasing the shop items corresponding to Aural Decoy, Farron Flashsword, Spook (starting items), and Pestilent Mist (after giving one scroll).
FS: Smough's Armor - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Gauntlets - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Helm - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Leggings - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Sneering Mask - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Soothing Sunlight - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Soul Arrow - OrbeckSold by Orbeck
FS: Soul Arrow - Yoel/Yuria shopSold by Yoel/Yuria
FS: Soul Arrow - shopSold by Handmaid
FS: Soul Greatsword - OrbeckSold by Orbeck
FS: Soul Greatsword - Yoel/Yuria shopSold by Yoel/Yuria after using Draw Out True Strength
FS: Soul Spear - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Soul of a Deserted Corpse - bell tower doorNext to the door requiring the Tower Key
FS: Spook - OrbeckSold by Orbeck
FS: Storm Curved Sword - Ludleth for NamelessBoss weapon for Nameless King
FS: Sunless Armor - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Gauntlets - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Leggings - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Talisman - Sirris, kill GA bossDropped by Sirris on death or quest completion.
FS: Sunless Veil - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunlight Spear - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Sunset Shield - by grave after killing Hodrick w/SirrisLeft by Sirris upon quest completion.
FS: Tears of Denial - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Titanite Scale - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Titanite Slab - shop after placing all CindersSold by Handmaid after placing all Cinders of a Lord on their thrones
FS: Tower Key - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Twinkling Titanite - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Twisted Wall of Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Uchigatana - NPC dropDropped by Sword Master
FS: Undead Legion Armor - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Gauntlet - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Helm - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Leggings - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Untrue Dark Ring - Yuria shopSold by Yuria
FS: Untrue White Ring - Yuria shopSold by Yuria
FS: Vordt's Great Hammer - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Vow of Silence - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Washing Pole - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: White Dragon Breath - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: White Sign Soapstone - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Wolf Knight's Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Wolf Ring+2 - left of boss room exitAfter Iudex Gundyr on the left
FS: Wolnir's Crown - shop after killing CC bossSold by Handmaid after defeating High Lord Wolnir
FS: Wolnir's Holy Sword - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Wood Grain Ring - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Xanthous Gloves - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Overcoat - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Trousers - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Yhorm's Great Machete - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Yhorm's Greatshield - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Young Dragon Ring - Orbeck for one scroll and buying three spellsGiven by Orbeck after purchasing four sorceries from him, and giving him one scroll, as a non-sorcerer.
FSBT: Armor of the Sun - crow for SiegbräuTrade Siegbräu with crow
FSBT: Blessed Gem - crow for Moaning ShieldTrade Moaning Shield with crow
FSBT: Covetous Silver Serpent Ring - illusory wall past raftersFrom the Firelink Shrine roof, past the rafters and an illusory wall
FSBT: Estus Ring - tower baseDropping down from the Bell Tower to where Irina eventually resides
FSBT: Estus Shard - raftersIn the Firelink Shrine rafters, accessible from the roof
FSBT: Fire Keeper Gloves - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Robe - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Skirt - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Soul - tower topAt the top of the Bell Tower
FSBT: Hello Carving - crow for Alluring SkullTrade Alluring Skull with crow
FSBT: Help me! Carving - crow for any sacred chimeTrade any Sacred Chime with crow
FSBT: Hollow Gem - crow for EleonoraTrade Eleonora with crow
FSBT: Homeward Bone - roofOn Firelink Shrine roof
FSBT: I'm sorry Carving - crow for Shriving StoneTrade Shriving Stone with crow
FSBT: Iron Bracelets - crow for Homeward BoneTrade Homeward Bone with crow
FSBT: Iron Helm - crow for Lightning UrnTrade Lightning Urn with crow
FSBT: Iron Leggings - crow for Seed of a Giant TreeTrade Seed of a Giant Tree with crow
FSBT: Large Titanite Shard - crow for FirebombTrade Firebomb or Rope Firebomb with crow
FSBT: Lightning Gem - crow for Xanthous CrownTrade Xanthous Crown with crow
FSBT: Lucatiel's Mask - crow for Vertebra ShackleTrade Vertebra Shackle with crow
FSBT: Porcine Shield - crow for Undead Bone ShardTrade Undead Bone Shard with crow
FSBT: Ring of Sacrifice - crow for Loretta's BoneTrade Loretta's Bone with crow
FSBT: Sunlight Shield - crow for Mendicant's StaffTrade Mendicant's Staff with crow
FSBT: Thank you Carving - crow for Hidden BlessingTrade Hidden Blessing with crow
FSBT: Titanite Chunk - crow for Black FirebombTrade Black Firebomb or Rope Black Firebomb with crow
FSBT: Titanite Scale - crow for Blacksmith HammerTrade Blacksmith Hammer with crow
FSBT: Titanite Slab - crow for Coiled Sword FragmentTrade Coiled Sword Fragment with crow
FSBT: Twinkling Titanite - crow for Large Leather ShieldTrade Large Leather Shield with crow
FSBT: Twinkling Titanite - crow for Prism StoneTrade Prism Stone with crow
FSBT: Twinkling Titanite - lizard behind FirelinkDropped by the Crystal Lizard behind Firelink Shrine. Can be accessed with tree jump by going all the way around the roof, left of the entrance to the rafters, or alternatively dropping down from the Bell Tower.
FSBT: Very good! Carving - crow for Divine BlessingTrade Divine Blessing with crow
GA: Avelyn - 1F, drop from 3F onto bookshelvesOn top of a bookshelf on the Archive first floor, accessible by going halfway up the stairs to the third floor, dropping down past the Grand Archives Scholar, and then dropping down again
GA: Blessed Gem - raftersOn the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Chaos Gem - dark room, lizardDropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Cinders of a Lord - Lothric PrinceDropped by Twin Princes
GA: Crystal Chime - 1F, path from wax poolOn the Archives first floor, in the room with the Lothric Knight, to the right
GA: Crystal Gem - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Crystal Scroll - 2F late, miniboss dropDropped by the Grand Archives Crystal Sage
GA: Divine Blessing - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Divine Pillars of Light - cage above raftersIn a cage above the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Ember - 5F, by entranceOn a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, on the opposite side from the wax pool
GA: Estus Shard - dome, far balconyOn the Archives roof near the three Winged Knights, in a side area overlooking the ocean.
GA: Fleshbite Ring - up stairs from 4FFrom the first shortcut elevator with the movable bookshelf, past the Scholars right before going outside onto the roof, in an alcove to the right with many Clawed Curse bookshelves
GA: Golden Wing Crest Shield - outside 5F, NPC dropDropped by Lion Knight Albert before the stairs leading up to Twin Princes
GA: Heavy Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Hollow Gem - rooftops lower, in hallGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, in a tunnel underneath the ledge
GA: Homeward Bone - 2F early balconyOn the Archives second floor, on the balcony with the ladder going up to the Crystal Sage
GA: Hunter's Ring - dome, very topAt the top of the ladder in roof the area with the Winged Knights
GA: Large Soul of a Crestfallen Knight - 4F, backIn the back of a Clawed Curse-heavy corridor of bookshelves, in the area with the Grand Archives Scholars and dropdown ladder, after the first shortcut elevator with the movable bookshelf
GA: Large Soul of a Crestfallen Knight - outside 5FIn the middle of the area with the three human NPCs attacking you, before the Grand Archives bonfire shortcut elevator
GA: Lingering Dragoncrest Ring+2 - dome, room behind spireNear the tower with the Winged Knights, up the stairs on the opposite side from the ladder leading up to the Hunter's Ring
GA: Onikiri and Ubadachi - outside 5F, NPC dropDropped by Black Hand Kamui before the stairs leading up to Twin Princes
GA: Outrider Knight Armor - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Gauntlets - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Helm - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Leggings - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Power Within - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
GA: Refined Gem - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Sage Ring+1 - rafters, second level downOn the rafters high above the Grand Archives, dropping down from the cage to the high rafters to the rafters below with the Corpse-grub
GA: Sage's Crystal Staff - outside 5F, NPC dropDropped by Daughter of Crystal Kriemhild before the stairs leading up to Twin Princes
GA: Scholar Ring - 2F, between late and earlyOn the corpse of a sitting Archives Scholar between two bookshelves, accessible by activating a lever before crossing the bridge that is the Crystal Sage's final location
GA: Sharp Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Shriving Stone - 2F late, by ladder from 3FGoing from the Crystal Sage's location on the third floor to its location on the bridge, after descending the ladder
GA: Soul Stream - 3F, behind illusory wallPast the Crystal Sage's third floor location, an illusory wall, and an Outrider Knight, on the corpse of a sitting Archives Scholar
GA: Soul of a Crestfallen Knight - 1F, loop left after dropOn the Archives first floor, hugging the left wall, on a ledge that loops back around to the left wall
GA: Soul of a Crestfallen Knight - path to domeOn balcony of the building with the second shortcut elevator down to the bonfire, accessible by going up the spiral stairs to the left
GA: Soul of a Nameless Soldier - dark roomOn the Archives first floor, after the wax pool, against a Clawed Curse bookshelf
GA: Soul of a Weary Warrior - rooftops, by lizardsOn the Archives roof, going up the first rooftop slope where a Gargoyle always attacks you
GA: Soul of the Twin PrincesDropped by Twin Princes
GA: Titanite Chunk - 1F, balconyOn the Archives first floor, on balcony overlooking the entrance opposite from the Grand Archives Scholars wax pool
GA: Titanite Chunk - 1F, path from wax poolOn the Archives first floor, toward the Lothric Knight, turning right to a ledge leading back to the entrance area
GA: Titanite Chunk - 1F, up right stairsGoing right after entering the Archives entrance and up the short flight of stairs
GA: Titanite Chunk - 2F, by wax poolUp the stairs from the Archives second floor on the right side from the entrance, in a corner near the small wax pool
GA: Titanite Chunk - 2F, right after dark roomExiting from the dark room with the Crystal Lizards on the first floor onto the second floor main room, then taking an immediate right
GA: Titanite Chunk - 5F, far balconyOn a balcony outside where Lothric Knight stands on the top floor of the Archives, accessing by going right from the final wax pool or by dropping down from the gargoyle area
GA: Titanite Chunk - rooftops lower, ledge by buttressGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, on a roof ledge to the right
GA: Titanite Chunk - rooftops, balconyGoing onto the roof and down the first ladder, all the way down the ledge facing the ocean to the right
GA: Titanite Chunk - rooftops, just before 5FOn the Archives roof, after a short dropdown, in the small area where the two Gargoyles attack you
GA: Titanite Scale - 1F, drop from 2F late onto bookshelves, lizardDropped by a Crystal Lizard on first floor bookshelves. Can be accessed by dropping down to the left at the end of the bridge which is the Crystal Sage's final location
GA: Titanite Scale - 1F, up stairs on bookshelfOn the Archives first floor, up a movable set of stairs near the large wax pool, on top of a bookshelf
GA: Titanite Scale - 2F, titanite scale atop bookshelfOn top of a bookshelf on the Archive second floor, accessible by going halfway up the stairs to the third floor and dropping down near a Grand Archives Scholar
GA: Titanite Scale - 3F, by ladder to 2F lateGoing from the Crystal Sage's location on the third floor to its location on the bridge, on the left side of the ladder you descend, behind a table
GA: Titanite Scale - 3F, corner up stairsFrom the Grand Archives third floor up past the thralls, in a corner with bookshelves to the left
GA: Titanite Scale - 4F, chest by exitIn a chest after the first elevator shortcut with the movable bookshelf, in the area with the Grand Archives Scholars, to the left of the stairwell leading up to the roof
GA: Titanite Scale - dark room, upstairsRight after going up the stairs to the Archives second floor, on the left guarded by a Grand Archives Scholar and a sequence of Clawed Curse bookshelves
GA: Titanite Scale - rooftops lower, path to 2FGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, then going past the corvians all the way to the left and making a jump
GA: Titanite Slab - 1F, after pulling 2F switchIn a chest on the Archives first floor, behind a bookshelf moved by pulling a lever in the middle of the second floor between two cursed bookshelves
GA: Titanite Slab - dome, kill all mobsDropped by killing all three Winged Knights on top of the Archives
GA: Titanite Slab - final elevator secretAt the bottom of the shortcut elevator right outside the Twin Princes fight. Requires sending the elevator up to the top from the middle, and then riding the lower elevator down.
GA: Twinkling Titanite - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Twinkling Titanite - 2F, lizard by entranceDropped by the Crystal Lizard on the Archives second floor, going toward the stairs/balcony
GA: Twinkling Titanite - dark room, lizard #1Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - dark room, lizard #2Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Twinkling Titanite - rooftops, lizard #1Dropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - rooftops, lizard #2Dropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Undead Bone Shard - 5F, by entranceOn the corpse of a sitting Archives Scholar on a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, near the final wax pool
GA: Witch's Locks - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
HWL: Astora Straight Sword - fort walkway, drop downIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel, dropping down past the edge
HWL: Basin of Vows - EmmaDropped by Emma upon killing her. This is possible to do at any time
HWL: Battle Axe - flame tower, mimicDropped by mimic in the building guarded by the fire-breathing wyvern
HWL: Binoculars - corpse tower, upper platformIn the area with the dead wyvern, at the top of a set of stairs past a Hollow Soldier
HWL: Black Firebomb - small roof over fountainAfter roof with Pus of Man, on the edge of another rooftop to the left where you can drop down into Winged Knight area
HWL: Broadsword - fort, room off walkwayIn the building with the Pus of Man on the roof, past the Lothric Knight in an alcove to the left
HWL: Cell Key - fort ground, down stairsIn the basement of the building with Pus of Man on the roof, down the stairs guarded by a dog
HWL: Claymore - flame plazaIn the area where the wyvern breathes fire, farthest away from the door
HWL: Club - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Ember - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Ember - fountain #1In the area with the Winged Knight
HWL: Ember - fountain #2In the area with the Winged Knight
HWL: Estus Shard - fort ground, on anvilIn the basement of the building with the Pus of Man on the roof, on the blacksmith anvil
HWL: Firebomb - corpse tower, under tableIn the building near the dead wyvern, behind a table near the ladder you descend
HWL: Firebomb - fort roofNext to the Pus of Man on the roof
HWL: Firebomb - top of ladder to fountainBy the long ladder leading down to the area with the Winged Knight
HWL: Firebomb - wall tower, beamIn the building with the Tower on the Wall bonfire, on a wooden beam overhanging the lower levels
HWL: Fleshbite Ring+1 - fort roof, jump to other roofJumping from the roof with the Pus of Man to a nearby building with a fenced roof
HWL: Gold Pine Resin - corpse tower, dropDropping past the dead wyvern, down the left path from the High Wall bonfire
HWL: Green Blossom - fort walkway, hall behind wheelIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel
HWL: Green Blossom - shortcut, lower courtyardIn the courtyard at the bottom of the shortcut elevator
HWL: Large Soul of a Deserted Corpse - flame plazaIn the area where the wyvern breathes fire, behind one of the praying statues
HWL: Large Soul of a Deserted Corpse - fort roofOn the edge of the roof with the Pus of Man
HWL: Large Soul of a Deserted Corpse - platform by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area)
HWL: Longbow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Lucerne - promenade, side pathOn one of the side paths from the main path connecting Dancer and Vordt fights, patrolled by a Lothric Knight
HWL: Mail Breaker - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire on the roof, before Greirat's cell
HWL: Rapier - fountain, cornerIn a corner in the area with the Winged Knight
HWL: Raw Gem - fort roof, lizardDropped by the Crystal Lizard on the rooftop after the Tower on the Wall bonfire
HWL: Red Eye Orb - wall tower, minibossDropped by the Darkwraith past the Lift Chamber Key
HWL: Refined Gem - promenade minibossDropped by the red-eyed Lothric Knight to the left of the Dancer's room entrance
HWL: Ring of Sacrifice - awning by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area), jumping onto a wooden support
HWL: Ring of the Evil Eye+2 - fort ground, far wallIn the basement of the building with the Pus of Man on the roof, on the far wall past the stairwell, behind some barrels
HWL: Silver Eagle Kite Shield - fort mezzanineIn the chest on the balcony overlooking the basement of the building with the Pus of Man on the roof
HWL: Small Lothric Banner - EmmaGiven by Emma, or dropped upon death
HWL: Soul of Boreal Valley VordtDropped by Vordt of the Boreal Valley
HWL: Soul of a Deserted Corpse - by wall tower doorRight before the entrance to the building with the Tower on the Wall bonfire
HWL: Soul of a Deserted Corpse - corpse tower, bottom floorDown the ladder of the building near the dead wyvern, on the way to the living wyvern
HWL: Soul of a Deserted Corpse - fort entry, cornerIn the corner of the room with a Lothric Knight, with the Pus of Man on the roof
HWL: Soul of a Deserted Corpse - fountain, path to promenadeIn between the Winged Knight area and the Dancer/Vordt corridor
HWL: Soul of a Deserted Corpse - path to back tower, by lift doorWhere the Greataxe Hollow Soldier patrols outside of the elevator shortcut entrance
HWL: Soul of a Deserted Corpse - path to corpse towerAt the very start, heading left from the High Wall bonfire
HWL: Soul of a Deserted Corpse - wall tower, right of exitExiting the building with the Tower on the Wall bonfire on the roof, immediately to the right
HWL: Soul of the DancerDropped by Dancer of the Boreal Valley
HWL: Standard Arrow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Throwing Knife - shortcut, lift topAt the top of the elevator shortcut, opposite from the one-way door
HWL: Throwing Knife - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire, in the room with the explosive barrels
HWL: Titanite Shard - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Titanite Shard - fort ground behind cratesBehind some wooden crates in the basement of the building with the Pus of Man on the roof
HWL: Titanite Shard - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Titanite Shard - fort, room off entryIn the building with the Pus of Man on the roof, in a room to the left and up the short stairs
HWL: Titanite Shard - wall tower, corner by bonfireOn the balcony with the Tower on the Wall bonfire
HWL: Undead Hunter Charm - fort, room off entry, in potIn the building with the Pus of Man on the roof, in a room to the left, in a pot you have to break
HWL: Way of Blue - EmmaGiven by Emma or dropped upon death.
IBV: Blood Gem - descent, platform before lakeIn front of the tree in the courtyard before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Blue Bug Pellet - ascent, in last buildingIn the final building before Pontiff's cathedral, coming from the sewer, on the first floor
IBV: Blue Bug Pellet - descent, dark roomIn the dark area with the Irithyllian slaves, to the left of the staircase
IBV: Budding Green Blossom - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Chloranthy Ring+1 - plaza, behind altarIn the area before and below Pontiff's cathedral, behind the central structure
IBV: Covetous Gold Serpent Ring+1 - descent, drop after dark roomAfter the dark area with the Irithyllian slaves, drop down to the right
IBV: Creighton's Steel Mask - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Divine Blessing - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Divine Blessing - great hall, mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Dorhys' Gnawing - Dorhys dropDropped by Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches and to the left
IBV: Dragonslayer's Axe - Creighton dropFollowing Sirris' questline, dropped by Creighton the Wanderer when he invades in the graveyard after the Church of Yorshka.
IBV: Dung Pie - sewer #1In the area with the sewer centipedes
IBV: Dung Pie - sewer #2In the area with the sewer centipedes
IBV: Ember - shortcut from church to cathedralAfter the gate shortcut from Church of Yorshka to Pontiff's cathedral
IBV: Emit Force - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Excrement-covered Ashes - sewer, by stairsIn the area with the sewer centipedes, before going up the stairs to the kitchen
IBV: Fading Soul - descent, cliff edge #1In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Fading Soul - descent, cliff edge #2In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Great Heal - lake, dead Corpse-GrubOn the Corpse-grub at the edge of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake wallOn the wall of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake, by Distant ManorIn the lake close to the Distant Manor bonfire
IBV: Green Blossom - lake, by stairs from descentGoing down the stairs into the lake leading to the Distant Manor bonfire
IBV: Homeward Bone - descent, before gravestoneIn the graveyard down the stairs from the Church of Yorshka, in front of the grave with the Corvian
IBV: Kukri - descent, side pathDown the stairs from the graveyard after Church of Yorshka, before the group of dogs in the left path
IBV: Large Soul of a Nameless Soldier - ascent, after great hallBy the tree near the stairs from the sewer leading up to Pontiff's cathedral, where the first dogs attack you
IBV: Large Soul of a Nameless Soldier - central, by bonfireBy the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - lake islandOn an island in the lake leading to the Distant Manor bonfire
IBV: Large Soul of a Nameless Soldier - path to plazaOn the path from Central Irithyll bonfire, before making the left toward Church of Yorshka
IBV: Large Titanite Shard - Distant Manor, under overhangUnder overhang next to second set of stairs leading from Distant Manor bonfire
IBV: Large Titanite Shard - ascent, by elevator doorOn the path from the sewer leading up to Pontiff's cathedral, to the right of the statue surrounded by dogs
IBV: Large Titanite Shard - ascent, down ladder in last buildingOutside the final building before Pontiff's cathedral, coming from the sewer, dropping down to the left before the entrance
IBV: Large Titanite Shard - central, balcony just before plazaFrom the Central Irithyll bonfire, on the balcony with the second Fire Witch.
IBV: Large Titanite Shard - central, side path after first fountainUp the stairs from the Central Irithyll bonfire, on a railing to the right
IBV: Large Titanite Shard - great hall, main floor mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Large Titanite Shard - great hall, upstairs mob drop #1One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - great hall, upstairs mob drop #2One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - path to DorhysBefore the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Large Titanite Shard - plaza, balcony overlooking ascentOn the path from Central Irithyll bonfire, instead of going left toward the Church of Yorshka, going right, on the balcony
IBV: Large Titanite Shard - plaza, by stairs to churchTo the left of the stairs leading up to the Church of Yorshka from Central Irithyll
IBV: Leo Ring - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Lightning Gem - plaza centerIn the area before and below Pontiff's cathedral, in the center guarded by the enemies
IBV: Magic Clutch Ring - plaza, illusory wallIn the area before and below Pontiff's cathedral, behind an illusory wall to the right
IBV: Mirrah Chain Gloves - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Leggings - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Mail - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Proof of a Concord Kept - Church of Yorshka altarAt the altar in the Church of Yorshka
IBV: Rime-blue Moss Clump - central, by bonfireBy the Central Irithyll bonfire
IBV: Rime-blue Moss Clump - central, past second fountainFrom the Central Irithyll bonfire, to the left before the first Fire Witch.
IBV: Ring of Sacrifice - lake, right of stairs from descentNear the sewer centipede at the start of the lake leading to the Distant Manor bonfire
IBV: Ring of the Evil Eye - AnriGiven by Anri of Astora in the Church of Yorshka, or if told of Horace's whereabouts in the Catacombs
IBV: Ring of the Sun's First Born - fall from in front of cathedralDropping down from in front of Pontiff Sulyvahn's church toward the Church of Yorshka
IBV: Roster of Knights - descent, first landingOn the landing going down the stairs from Church of Yorshka to the graveyard
IBV: Rusted Gold Coin - Distant Manor, drop after stairsDropping down after the first set of stairs leading from Distant Manor bonfire
IBV: Rusted Gold Coin - descent, side pathDown the stairs from the graveyard after Church of Yorshka, guarded by the group of dogs in the left path
IBV: Shriving Stone - descent, dark room raftersOn the rafters in the dark area with the Irithyllian slaves
IBV: Siegbräu - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Smough's Great Hammer - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Soul of Pontiff SulyvahnDropped by Pontiff Sulyvahn
IBV: Soul of a Weary Warrior - ascent, by final staircaseToward the end of the path from the sewer leading up to Pontiff's cathedral, to the left of the final staircase
IBV: Soul of a Weary Warrior - central, by first fountainBy the Central Irithyll bonfire
IBV: Soul of a Weary Warrior - central, railing by first fountainOn the railing overlooking the Central Irithyll bonfire, at the very start
IBV: Soul of a Weary Warrior - plaza, side room lowerDropping down from the path from Church of Yorshka to Pontiff, guarded by the pensive Fire Witch
IBV: Soul of a Weary Warrior - plaza, side room upperIn the path from Church of Yorshka to Pontiff's cathedral, at the broken ledge you can drop down onto the Fire Witch
IBV: Twinkling Titanite - central, lizard before plazaDropped by a Crystal Lizard past the Central Irithyll Fire Witches and to the left
IBV: Twinkling Titanite - descent, lizard behind illusory wallDropped by a Crystal Lizard behind an illusory wall before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Undead Bone Shard - descent, behind gravestoneIn the graveyard down the stairs from the Church of Yorshka, behind the grave with the Corvian
IBV: Witchtree Branch - by DorhysIn the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Wood Grain Ring+2 - ascent, right after great hallLeaving the building with the Silver Knight staring at the painting, instead of going left up the stairs, go right
IBV: Yorshka's Spear - descent, dark room rafters chestIn a chest in the rafters of the dark area with the Irithyllian slaves
ID: Alva Armor - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Gauntlets - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Helm - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Leggings - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Bellowing Dragoncrest Ring - drop from B1 towards pitDropping down from the Jailbreaker's Key shortcut at the end of the top corridor on the bonfire side in Irithyll Dungeon
ID: Covetous Gold Serpent Ring - Siegward's cellIn the Old Cell where Siegward is rescued
ID: Covetous Silver Serpent Ring+1 - pit lift, middle platformOn one of the platforms in elevator shaft of the shortcut elevator from the Giant Slave area to the Irithyll Dungeon bonfire
ID: Dark Clutch Ring - stairs between pit and B3, mimicDropped by the mimic found going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs, on the left side
ID: Dragon Torso Stone - B3, outside liftOn the balcony corpse in the Path of the Dragon pose
ID: Dragonslayer Lightning Arrow - pit, mimic in hallDropped by the mimic in the side corridor from where the Giant Slave is standing, before the long ladder
ID: Dung Pie - B3, by path from pitIn the room with the Giant Hound Rats
ID: Dung Pie - pit, miniboss dropDrop from the Giant Slave
ID: Dusk Crown Ring - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
ID: Ember - B3 centerAt the center pillar in the main Jailer cell block
ID: Ember - B3 far rightIn the main Jailer cell block, on the left side coming from the Profaned Capital
ID: Estus Shard - mimic on path from B2 to pitDropped by the mimic in the room after the outside area of Irithyll Dungeon overlooking Profaned Capital
ID: Fading Soul - B1 near, main hallOn the top corridor on the bonfire side in Irithyll Dungeon, close to the first Jailer
ID: Great Magic Shield - B2 near, mob drop in far left cellOne-time drop from the Infested Corpse in the bottom corridor on the bonfire side of Irithyll Dungeon, in the closest cell
ID: Homeward Bone - path from B2 to pitIn the part of Irithyll Dungeon overlooking the Profaned Capital, after exiting the last jail cell corridor
ID: Jailbreaker's Key - B1 far, cell after gateIn the cell of the top corridor opposite to the bonfire in Irithyll Dungeon
ID: Large Soul of a Nameless Soldier - B2 far, by liftTaking the elevator up from the area you can use Path of the Dragon, before the one-way door
ID: Large Soul of a Nameless Soldier - B2, hall by stairsAt the end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Large Soul of a Weary Warrior - just before Profaned CapitalIn the open area before the bridge leading into Profaned Capital from Irithyll Dungeon
ID: Large Titanite Shard - B1 far, rightmost cellIn a cell on the far end of the top corridor opposite to the bonfire in Irithyll Dungeon, nearby the Jailer
ID: Large Titanite Shard - B1 near, by doorAt the end of the top corridor on the bonfire side in Irithyll Dungeon, before the Jailbreaker's Key door
ID: Large Titanite Shard - B3 near, right cornerIn the main Jailer cell block, to the left of the hallway leading to the Path of the Dragon area
ID: Large Titanite Shard - after bonfire, second cell on leftIn the second cell on the right after Irithyll Dungeon bonfire
ID: Large Titanite Shard - pit #1On the floor where the Giant Slave is standing
ID: Large Titanite Shard - pit #2On the floor where the Giant Slave is standing
ID: Lightning Blade - B3 lift, middle platformOn the middle platform riding the elevator up from the Path of the Dragon area
ID: Lightning Bolt - awning over pitOn the wooden overhangs above the Giant Slave. Can be reached by dropping down after climbing the long ladder around the area where the Giant stands.
ID: Murakumo - Alva dropDropped by Alva, Seeker of the Spurned when he invades in the cliffside path to Irithyll Dungeon
ID: Old Cell Key - stairs between pit and B3In a chest found going past the Giant Slave to the sewer with the rats and the basilisks, up the stairs to the end, on the right side
ID: Old Sorcerer Boots - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Coat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Gauntlets - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Hat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Pale Pine Resin - B1 far, cell with broken wallIn the jail cell with the broken wall in the top corridor opposite to the bonfire in Irithyll Dungeon, near the passive Wretch on the wall
ID: Pickaxe - path from pit to B3Passing by the Giant Slave, before the tunnel with the rats and basilisks
ID: Prisoner Chief's Ashes - B2 near, locked cell by stairsIn the cell at the far end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Profaned Coal - B3 far, left cellIn the room with the Wretches next to the main Jailer cell block, guarded by a Wretch
ID: Profaned Flame - pitOn the floor where the Giant Slave is standing
ID: Rusted Coin - after bonfire, first cell on leftIn the first cell on the left from the Irithyll dungeon bonfire
ID: Rusted Gold Coin - after bonfire, last cell on rightIn the third cell on the right from the Irithyll Dungeon bonfire
ID: Simple Gem - B2 far, cell by stairsIn the cell near the bottom corridor opposite to the bonfire in Irithyll Dungeon, adjacent to the room with three Jailers and Cage Spiders
ID: Soul of a Crestfallen Knight - balcony above pitUnder whether the Giant Slave is resting his head
ID: Soul of a Weary Warrior - by drop to pitAt the end of the room with many peasant hollows after the Estus Shard mimic
ID: Soul of a Weary Warrior - stairs between pit and B3Going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs
ID: Titanite Chunk - balcony above pit, lizardDropped by the Crystal Lizard where the Giant Slave is resting his head
ID: Titanite Chunk - pit, miniboss dropDrop from the Giant Slave
ID: Titanite Scale - B2 far, lizardDropped by the Crystal Lizard on the bottom corridor opposite from the bonfire in Irithyll Dungeon where a Wretch attacks you
ID: Titanite Scale - B3 far, mimic in hallDropped by the mimic in the main Jailer cell block
ID: Titanite Slab - SiegwardGiven by Siegward after unlocking Old Cell or on quest completion
ID: Xanthous Ashes - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
KFF: Soul of the LordsDropped by Soul of Cinder
LC: Black Firebomb - dark room lowerIn the room with the firebomb-throwing hollows, against the wall on the lowest level
LC: Braille Divine Tome of Lothric - wyvern roomIn the room next to the second Pus of Man wyvern
LC: Caitha's Chime - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, and then dropping down again towards the Lothric knights
LC: Dark Stoneplate Ring+1 - wyvern room, balconyThrough the room next to the second Pus of Man wyvern, on the balcony outside
LC: Ember - by Dragon Barracks bonfireNear the Dragon Barracks bonfire
LC: Ember - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Ember - main hall, left of stairsTo the left of the stairs past the Dragon Barracks grate
LC: Ember - plaza centerIn the area where the Pus of Man wyverns breathe fire
LC: Ember - plaza, by gateOn the railing near the area where the Pus of Man wyverns breathe fire, before the gate
LC: Ember - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Grand Archives Key - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Greatlance - overlooking Dragon Barracks bonfireGuarded by a pensive Lothric Knight after the Dragon Barracks bonfire and continuing up the stairs
LC: Hood of PrayerIn a chest right after the Lothric Castle bonfire
LC: Irithyll Rapier - basement, miniboss dropDropped by the Boreal Outrider Knight in the basement
LC: Knight's Ring - altarClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, past the Large Hollow Soldier, down into the room with the tables
LC: Large Soul of a Nameless Soldier - dark room midIn the room with the firebomb-throwing hollows, up the ladder
LC: Large Soul of a Nameless Soldier - moat, right pathFound on the ledge after dropping into the area with the Pus of Man transforming hollows and making the entire loop
LC: Large Soul of a Nameless Soldier - plaza left, by pillarIn the building to the left of the area where the Pus of Man wyverns breathe fire, against a pillar
LC: Large Soul of a Weary Warrior - ascent, last turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs and forwards
LC: Large Soul of a Weary Warrior - main hall, by leverOn a ledge to the right of the lever opening the grate
LC: Life Ring+2 - dark room mid, out door opposite wyvern, drop downPast the room with the firebomb-throwing hollows and Pus of Man wyvern, around to the front, dropping down past where the Titanite Chunk is
LC: Lightning Urn - moat, right path, first roomStarting the loop from where the Pus of Man hollows transform, behind some crates in the first room
LC: Lightning Urn - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Pale Pine Resin - dark room upper, by mimicIn the room with the firebomb-throwing hollows, next to the mimic in the far back left
LC: Raw Gem - plaza leftOn a balcony to the left of the area where the Pus of Man wyverns breathe fire, where the Hollow Soldier throws Undead Hunter Charms
LC: Red Tearstone Ring - chapel, drop onto roofFrom the chapel to the right of the Dragonslayer Armour fight, on the balcony to the left
LC: Refined Gem - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Robe of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Rusted Coin - chapelIn the chapel to the right of the Dragonslayer Armour fight
LC: Sacred Bloom Shield - ascent, behind illusory wallUp the ladder where the Winged Knight is waiting, past an illusory wall
LC: Skirt of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Sniper Bolt - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Sniper Crossbow - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Soul of Dragonslayer ArmourDropped by Dragonslayer Armour
LC: Soul of a Crestfallen Knight - by lift bottomGuarded by a buffed Lothric Knight straight from the Dancer bonfire
LC: Soul of a Crestfallen Knight - wyvern room, balconyOn a ledge accessible after the second Pus of Man wyvern is defeated
LC: Spirit Tree Crest Shield - basement, chestIn a chest in the basement with the Outrider Knight
LC: Sunlight Medal - by lift topNext to the shortcut elevator outside of the Dragonslayer Armour fight that goes down to the start of the area
LC: Sunlight Straight Sword - wyvern room, mimicDropped by the mimic in the room next to the second Pus of Man wyvern
LC: Thunder Stoneplate Ring+2 - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, out on the edge
LC: Titanite Chunk - altar roofClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, overlooking the tree
LC: Titanite Chunk - ascent, final turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs, then right
LC: Titanite Chunk - ascent, first balconyRight after the Lothric Castle bonfire, out on the balcony
LC: Titanite Chunk - ascent, turret before barricadesFrom the Lothric Castle bonfire, up the stairs, straight, and then down the stairs behind the barricade
LC: Titanite Chunk - dark room mid, out door opposite wyvernFrom the room with the firebomb-throwing hollows, past the Pus of Man Wyvern and back around the front, before the Crystal Lizard
LC: Titanite Chunk - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Titanite Chunk - down stairs after bossDown the stairs to the right after Dragonslayer Armour
LC: Titanite Chunk - moat #1In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat #2In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat, near ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the near side to the bonfire
LC: Titanite Chunk - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Titanite Scale - altarIn a chest climbing the ladder to the rooftop outside the Dragonslayer Armour fight, continuing the loop past the Red-Eyed Lothric Knight
LC: Titanite Scale - basement, chestIn a chest in the basement with the Outrider Knight
LC: Titanite Scale - chapel, chestIn a chest in the chapel to the right of the Dragonslayer Armour fight
LC: Titanite Scale - dark room mid, out door opposite wyvernPassing through the room with the firebomb-throwing hollows and the Pus of Man wyvern around to the front, overlooking the area where the wyverns breathe fire
LC: Titanite Scale - dark room, upper balconyIn the room with the firebomb-throwing hollows, at the very top on a balcony to the right
LC: Titanite Scale - dark room, upper, mimicDropped by the crawling mimic at the top of the room with the firebomb-throwing hollows
LC: Twinkling Titanite - ascent, side roomIn the room where the Winged Knight drops down
LC: Twinkling Titanite - basement, chest #1In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - basement, chest #2In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - dark room mid, out door opposite wyvern, lizardDropped by the Crystal Lizard after the room with the firebomb-throwing hollows around the front
LC: Twinkling Titanite - moat, left sideBehind one of the Pus of Man transforming hollows, to the left of the bridge to the wyvern fire-breathing area
LC: Twinkling Titanite - moat, right path, lizardDropped by the Crystal Lizard near the thieves after dropping down to the area with the Pus of Man transforming hollows
LC: Undead Bone Shard - moat, far ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the far side from the bonfire
LC: Winged Knight Armor - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Gauntlets - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Helm - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Leggings - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
PC: Blooming Purple Moss Clump - walkway above swampAt the right end of the plank before dropping down into the Profaned Capital toxic pool
PC: Cinders of a Lord - Yhorm the GiantDropped by Yhorm the Giant
PC: Court Sorcerer Gloves - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Hood - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Robe - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Trousers - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer's Staff - chapel, mimic on second floorDropped by the mimic on the second floor of the Monstrosity of Sin building
PC: Cursebite Ring - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Eleonora - chapel ground floor, kill mobDropped by the Monstrosity of Sin on the first floor, furthest away from the door
PC: Ember - palace, far roomTo the right of the Profaned Flame, in the room with the many Jailers looking at the mimics
PC: Flame Stoneplate Ring+1 - chapel, drop from roof towards entranceDropping down from the roof connected to the second floor of the Monstrosity of Sin building, above the main entrance to the building
PC: Greatshield of Glory - palace, mimic in far roomDropped by the left mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Jailer's Key Ring - hall past chapelPast the Profaned Capital Court Sorcerer, in the corridor overlooking the Irithyll Dungeon Giant Slave area
PC: Large Soul of a Weary Warrior - bridge, far endOn the way from the Profaned Capital bonfire toward the Profaned Flame, crossing the bridge without dropping down
PC: Logan's Scroll - chapel roof, NPC dropDropped by the court sorcerer above the toxic pool
PC: Magic Stoneplate Ring+2 - tower baseAt the base of the Profaned Capital structure, going all the way around the outside wall clockwise
PC: Onislayer Greatarrow - bridgeItem on the bridge descending from the Profaned Capital bonfire into the Profaned Flame building
PC: Onislayer Greatbow - drop from bridgeFrom the bridge leading from the Profaned Capital bonfire to Yhorm, onto the ruined pillars shortcut to the right, behind you after the first dropdown.
PC: Pierce Shield - SiegwardDropped by Siegward upon death or quest completion, and sold by Patches while Siegward is in the well.
PC: Poison Arrow - chapel roofAt the far end of the roof with the Court Sorcerer
PC: Poison Gem - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Purging Stone - chapel ground floorAt the back of the room with the three Monstrosities of Sin on the first floor
PC: Purging Stone - swamp, by chapel ladderIn the middle of the Profaned Capital toxic pool, near the ladder to the Court Sorcerer
PC: Rubbish - chapel, down stairs from second floorHanging corpse visible from Profaned Capital accessible from the second floor of the building with the Monstrosities of Sin, in the back right
PC: Rusted Coin - below bridge #1Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - below bridge #2Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - tower exteriorTreasure visible on a ledge in the Profaned Capital bonfire. Can be accessed by climbing a ladder outside the main structure.
PC: Rusted Gold Coin - halls above swampIn the corridors leading to the Profaned Capital toxic pool
PC: Rusted Gold Coin - palace, mimic in far roomDropped by the right mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Shriving Stone - swamp, by chapel doorAt the far end of the Profaned Capital toxic pool, to the left of the door leading to the Monstrosities of Sin
PC: Siegbräu - Siegward after killing bossGiven by Siegward after helping him defeat Yhorm the Giant. You must talk to him before Emma teleports you.
PC: Soul of Yhorm the GiantDropped by Yhorm the Giant
PC: Storm Ruler - SiegwardDropped by Siegward upon death or quest completion.
PC: Storm Ruler - boss roomTo the right of Yhorm's throne
PC: Twinkling Titanite - halls above swamp, lizard #1Dropped by the second Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Twinkling Titanite - halls above swamp, lizard #2Dropped by the first Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Undead Bone Shard - by bonfireOn the corpse of Laddersmith Gilligan next to the Profaned Capital bonfire
PC: Wrath of the Gods - chapel, drop from roofDropping down from the roof of the Monstrosity of Sin building where the Court Sorcerer is
PW1: Black Firebomb - snowfield lower, path to bonfireDropping down after the first snow overhang and following the wall on the left, past the rotting bed descending toward the second bonfire
PW1: Blessed Gem - snowfield, behind towerBehind the Millwood Knight tower in the first area, approach from the right side
PW1: Budding Green Blossom - settlement courtyard, ledgeAfter sliding down the slope on the way to Corvian Settlement, dropping down hugging the left wall
PW1: Captain's Ashes - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder
PW1: Chillbite Ring - FriedeGiven by Sister Friede while she is sitting in the Ariandel Chapel, or on the stool after she moves.
PW1: Contraption Key - library, NPC dropDropped by Sir Vilhelm
PW1: Crow Quills - settlement loop, jump into courtyardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Go right and jump past some barrels onto the central platform.
PW1: Crow Talons - settlement roofs, near bonfireAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, into the building, then looping around onto its roof.
PW1: Dark Gem - settlement back, egg buildingDropping down to the right of the gate guarded by a Corvian Knight in Corvian Settlement, inside of the last building on the right
PW1: Ember - roots above depthsIn the tree branch area after climbing down the rope bridge, hugging a right wall past a Follower Javelin wielder
PW1: Ember - settlement main, left building after bridgeCrossing the bridge after Corvian Settlement bonfire, in the building to the left.
PW1: Ember - settlement, building near bonfireIn the first building in Corvian Settlement next to the bonfire building
PW1: Ethereal Oak Shield - snowfield tower, 3FIn the Millwood Knight tower on a Millwood Knight corpse, after climbing the first ladder, then going down the staircase
PW1: Follower Javelin - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Follower Sabre - roots above depthsOn a tree branch after climbing down the rope bridge. Rather than hugging a right wall toward a Follower Javelin wielder, drop off to the left.
PW1: Frozen Weapon - snowfield lower, egg zoneDropping down after the first snow overhang, in the rotting bed along the left side
PW1: Heavy Gem - snowfield villageBefore the Millwood Knight tower, on the far side of one of the ruined walls targeted by the archer
PW1: Hollow Gem - beside chapelTo the right of the entrance to the Ariandel
PW1: Homeward Bone - depths, up hillIn the Depths of the Painting, up a hill next to the giant crabs.
PW1: Homeward Bone - snowfield village, outcroppingDropping down after the first snow overhang and following the cliff on the right, making a sharp right after a ruined wall segment before approaching the Millwood Knight tower
PW1: Large Soul of a Weary Warrior - settlement hall roofOn top of the chapel with the Corvian Knight to the left of Vilhelm's building
PW1: Large Soul of a Weary Warrior - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder, on a Millwood Knight corpse
PW1: Large Soul of an Unknown Traveler - below snowfield village overhangUp the slope to the left of the Millwood Knight tower, dropping down after a snow overhang, then several more ledges.
PW1: Large Soul of an Unknown Traveler - settlement backIn Corvian Settlement, on the ground before the ladder climbing onto the rooftops
PW1: Large Soul of an Unknown Traveler - settlement courtyard, cliffAfter sliding down the slope on the way to Corvian Settlement, on a cliff to the right and behind
PW1: Large Soul of an Unknown Traveler - settlement loop, by bonfireCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. On the corpse in a hole in the wall leading back to the bonfire.
PW1: Large Soul of an Unknown Traveler - settlement roofs, balconyAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, on the other side of the bridge.
PW1: Large Soul of an Unknown Traveler - settlement, by ladder to bonfireTo the right of the ladder leading up to Corvian Settlement bonfire.
PW1: Large Soul of an Unknown Traveler - snowfield lower, by cliffDropping down after the first snow overhang, between the forest and the cliff edge, before where the large wolf drops down
PW1: Large Soul of an Unknown Traveler - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Large Soul of an Unknown Traveler - snowfield lower, path to villageDropping down after the first snow overhang and following the cliff on the right, on a tree past where the large wolf jumps down
PW1: Large Soul of an Unknown Traveler - snowfield upperGoing straight after the first bonfire, to the left of the caving snow overhand
PW1: Large Titanite Shard - lizard under bridge nearDropped by a Crystal Lizard after the Rope Bridge Cave on the way to Corvian Settlement
PW1: Large Titanite Shard - settlement loop, lizardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Hug the bonfire building's outer wall along the right side.
PW1: Large Titanite Shard - snowfield lower, left from fallDropping down after the first snow overhang, guarded by a Tree Woman overlooking the rotting bed along the left wall
PW1: Millwood Battle Axe - snowfield tower, 5FIn the Milkwood Knight tower, either dropping down from rafters after climbing the second ladder or making a risky jump
PW1: Millwood Greatarrow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Millwood Greatbow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Onyx Blade - library, NPC dropDropped by Sir Vilhelm
PW1: Poison Gem - snowfield upper, forward from bonfireFollowing the left wall from the start, guarded by a Giant Fly
PW1: Rime-blue Moss Clump - below bridge farIn a small alcove to the right after climbing down the rope bridge
PW1: Rime-blue Moss Clump - snowfield upper, overhangOn the first snow overhang at the start. It drops down at the same time you do.
PW1: Rime-blue Moss Clump - snowfield upper, starting caveIn the starting cave
PW1: Rusted Coin - right of libraryTo the right of Vilhelm's building
PW1: Rusted Coin - snowfield lower, straight from fallDropping down after the first snow overhang, shortly straight ahead
PW1: Rusted Gold Coin - settlement roofs, roof near second ladderAfter climbing the second ladder on the Corvian Settlement rooftops, immediately dropping off the bridge to the right, on a rooftop
PW1: Shriving Stone - below bridge nearAfter the Rope Bridge Cave bonfire, dropping down before the bridge, following the ledge all the way to the right
PW1: Simple Gem - settlement, lowest level, behind gateCrossing the bridge after Corvian Settlement bonfire, follow the left edge until a bridge, then drop down on the right side. Guarded by a Sewer Centipede.
PW1: Slave Knight Armor - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Gauntlets - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Hood - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Leggings - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Snap Freeze - depths, far end, mob dropIn the Depths of the Painting, past the giant crabs, guarded by a special Tree Woman. Killing her drops down a very long nearby ladder.
PW1: Soul of a Crestfallen Knight - settlement hall, raftersIn the rafters of the chapel with the Corvian Knight to the left of Vilhelm's building. Can drop down from the windows exposed to the roof.
PW1: Soul of a Weary Warrior - snowfield tower, 1FAt the bottom of the Millwood Knight tower on a Millwood Knight corpse
PW1: Titanite Slab - CorvianGiven by the Corvian NPC in the building next to Corvian Settlement bonfire.
PW1: Titanite Slab - depths, up secret ladderIn the Depths of the Painting, past the giant crabs, killing a special Tree Woman drops down a very long nearby ladder. Climb the ladder and also the ladder after that one.
PW1: Twinkling Titanite - roots, lizardDropped by a Crystal Lizard in the tree branch area after climbing down the rope bridge, before the ledge with the Follower Javelin wielder
PW1: Twinkling Titanite - settlement roofs, lizard before hallDropped by a Crystal Lizard on a bridge in Corvian Settlement before the rooftop of the chapel with the Corvian Knight inside.
PW1: Twinkling Titanite - snowfield tower, 3F lizardDropped by a Crystal Lizard in the Millwood Knight tower, climbing up the first ladder and descending the stairs down
PW1: Valorheart - boss dropDropped by Champion's Gravetender
PW1: Way of White Corona - settlement hall, by altarIn the chapel with the Corvian Knight to the left of Vilhelm's building, in front of the altar.
PW1: Young White Branch - right of libraryTo the right of Vilhelm's building
PW2: Blood Gem - B2, centerOn the lower level of the Ariandel Chapel basement, in the middle
PW2: Dung Pie - B1On the higher level of the Ariandel Chapel basement, on a wooden beam overlooking the lower level
PW2: Earth Seeker - pit caveIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, in the cave
PW2: Ember - pass, central alcoveAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, in a small alcove along the left wall
PW2: Floating Chaos - NPC dropDropped by Livid Pyromancer Dunnel when he invades while embered, whether boss is defeated or not. On the second level of Priscilla's building above the Gravetender fight, accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Follower Shield - pass, far cliffsideAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, on the cliff ledge past the open area, to the left
PW2: Follower Torch - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going all the way down the slope on the edge of the map. Guarded by a Follower
PW2: Homeward Bone - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Large Soul of a Crestfallen Knight - pit, by treeIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, by the tree
PW2: Large Titanite Shard - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going partway down the slope on the edge of the map
PW2: Large Titanite Shard - pass, just before B1On the way to Ariandel Chapel basement, past the Millwood Knights and before the first rotten tree that can be knocked down
PW2: Prism Stone - pass, tree by beginningUp the slope and to the left after the Snowy Mountain Pass, straight ahead by a tree
PW2: Pyromancer's Parting Flame - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Quakestone Hammer - pass, side path near B1On the way to Ariandel Chapel basement, rather than going right past the two Millwood Knights, go left, guarded by a very strong Millwood Knight
PW2: Soul of Sister FriedeDropped by Sister Friede
PW2: Soul of a Crestfallen Knight - pit edge #1In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Soul of a Crestfallen Knight - pit edge #2In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Titanite Chunk - pass, by kickable treeAfter the Snowy Mountain Pass bonfire, on a ledge to the right of the slope with the bell stuck in the ground, behind a tree
PW2: Titanite Chunk - pass, cliff overlooking bonfireOn a cliff overlooking the Snowy Mountain Pass bonfire. Requires following the left wall
PW2: Titanite Slab - boss dropOne-time drop after killing Father Ariandel and Friede (phase 2) for the first time.
PW2: Twinkling Titanite - B3, lizard #1Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Twinkling Titanite - B3, lizard #2Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Vilhelm's Armor - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Gauntlets - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's HelmOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Leggings - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
RC: Antiquated Plain Garb - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Black Witch Garb - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Hat - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Trousers - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Veil - swamp near right, by sunken churchTo the left of the submerged building with 4 Ringed Knights, near a spear-wielding knight.
RC: Black Witch Wrappings - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Blessed Gem - grave, down lowest stairsIn Shared Grave, after dropping down near Gael's flag and dropping down again, behind you. Or from the bonfire, go back through the side tunnel with the skeletons and down the stairs after that.
RC: Blindfold Mask - grave, NPC dropDropped by Moaning Knight (invades whether embered or not, or boss defeated or not) in Shared Grave.
RC: Blood of the Dark Soul - end boss dropDropped by Slave Knight Gael
RC: Budding Green Blossom - church pathOn the way to the Halflight building.
RC: Budding Green Blossom - wall top, flowers by stairsIn a patch of flowers to the right of the stairs leading up to the first Judicator along the left wall of the courtyard are Mausoleum Lookout.
RC: Budding Green Blossom - wall top, in flower clusterAlong the left wall of the courtyard after Mausoleum Lookout, in a patch of flowers.
RC: Chloranthy Ring+3 - wall hidden, drop onto statueFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, dropping back down toward the way to Filianore, onto a platform with a Gwyn statue. Try to land on the platform rather than the statue.
RC: Church Guardian Shiv - swamp far left, in buildingInside of the building at the remote end of the muck pit surrounded by praying Hollow Clerics.
RC: Covetous Gold Serpent Ring+3 - streets, by LappGoing up the very long ladder from the muck pit, then up some stairs, to the left, and across the bridge, in a building past the Ringed Knights. Also where Lapp can be found to tell him of the Purging Monument.
RC: Crucifix of the Mad King - ashes, NPC dropDropped by Shira, who invades you (ember not required) in the far-future version of her room
RC: Dark Gem - swamp near, by stairsIn the middle of the muck pit, close to the long stairs.
RC: Divine Blessing - streets monument, mob dropDropped by the Judicator near the Purging Monument area. Requires solving "Show Your Humanity" puzzle.
RC: Divine Blessing - wall top, mob dropDropped by the Judicator after the Mausoleum Lookup bonfire.
RC: Dragonhead Greatshield - lower cliff, under bridgeDown a slope to the right of the bridge where Midir first assaults you, past a sword-wielding Ringed Knight, under the bridge.
RC: Dragonhead Shield - streets monument, across bridgeFound in Purging Monument area, across the bridge from the monument. Requires solving "Show Your Humanity" puzzle.
RC: Ember - wall hidden, statue roomFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, in the room with the illusory statue.
RC: Ember - wall top, by statueAlong the left wall of the courtyard after Mausoleum Lookout, in front of a tall monument.
RC: Ember - wall upper, balconyOn the balcony attached to the room with the Ringed Inner Wall bonfire.
RC: Filianore's Spear Ornament - mid boss dropDropped by Halflight, Spear of the Church
RC: Filianore's Spear Ornament - wall hidden, by ladderNext the ladder leading down to the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Havel's Ring+3 - streets high, drop from building oppositeDropping down from the building where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Hidden Blessing - swamp center, mob dropDropped by Judicator patrolling the muck pit.
RC: Hidden Blessing - wall top, tomb under platformIn a tomb underneath the platform with the first Judicator, accessed by approaching from Mausoleum Lookout bonfire.
RC: Hollow Gem - wall upper, path to towerHeading down the cursed stairs after Ringed Inner Wall bonfire and another short flight of stairs, hanging on a balcony.
RC: Iron Dragonslayer Armor - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Gauntlets - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Helm - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Leggings - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Lapp's Armor - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Gauntlets - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Helm - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Leggings - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Large Soul of a Crestfallen Knight - streets monument, across bridgeFound in Purging Monument area, on the other side of the bridge leading to the monument. Requires solving "Show Your Humanity" puzzle.
RC: Large Soul of a Crestfallen Knight - streets, far stairsToward the bottom of the stairs leading down to the muck pit.
RC: Large Soul of a Weary Warrior - lower cliff, endToward the end of the upper path attacked Midir's fire-breathing.
RC: Large Soul of a Weary Warrior - swamp center, by peninsulaIn the muck pit approaching where the Judicator patrols from the stairs.
RC: Large Soul of a Weary Warrior - wall lower, past two illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past two illusory walls on the right side of the ascending stairs.
RC: Large Soul of a Weary Warrior - wall top, right of small tombIn the open toward the end of the courtyard after the Mausoleum Lookout bonfire, on the right side of the small tomb.
RC: Ledo's Great Hammer - streets high, opposite building, NPC dropDropped by Silver Knight Ledo (invades whether embered or not, or boss defeated or not) in the building down the path to the right after climbing the very long ladder from the muck area.
RC: Lightning Arrow - wall lower, past three illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past three illusory walls on the right side of the ascending stairs.
RC: Lightning Gem - grave, room after first dropIn Shared Grave, in the first room encountered after falling down from the crumbling stairs and continuing upward.
RC: Mossfruit - streets near left, path to gardenPartway down the stairs from Shira, across the bridge.
RC: Mossfruit - streets, far left alcoveNear the bottom of the stairs before the muck pit, in an alcove to the left.
RC: Preacher's Right Arm - swamp near right, by towerIn the muck pit behind a crystal-covered structure, close to the Ringed City Streets shortcut entrance.
RC: Prism Stone - swamp near, railing by bonfireOn the balcony of the path leading up to Ringed City Streets bonfire from the muck pit.
RC: Purging Stone - wall top, by door to upperAt the end of the path from Mausoleum Lookup to Ringed Inner Wall, just outside the door.
RC: Ring of the Evil Eye+3 - grave, mimicDropped by mimic in Shared Grave. In one of the rooms after dropping down near Gael's flag and then dropping down again.
RC: Ringed Knight Paired Greatswords - church path, mob dropDropped by Ringed Knight with paired greatswords before Filianore building.
RC: Ringed Knight Spear - streets, down far right hallIn a courtyard guarded by a spear-wielding Ringed Knight. Can be accessed from a hallway filled with cursed clerics on the right side going down the long stairs, or by climbing up the long ladder from the muck pit and dropping down past the Locust Preacher.
RC: Ringed Knight Straight Sword - swamp near, tower on peninsulaOn a monument next to the Ringed City Streets building. Can be easily accessed after unlocking the shortcut by following the left wall inside and then outside the building.
RC: Ritual Spear Fragment - church pathTo the right of the Paired Greatswords Ringed Knight on the way to Halflight.
RC: Rubbish - lower cliff, middleIn the middle of the upper path attacked Midir's fire-breathing, after the first alcove.
RC: Rubbish - swamp far, by crystalIn the remote end of the muck pit, next to a massive crystal structure between a giant tree and the building with praying Hollow Clerics, guarded by several Locust Preachers.
RC: Ruin Armor - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Gauntlets - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Helm - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Leggings - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Sacred Chime of Filianore - ashes, NPC dropGiven by Shira after accepting her request to kill Midir, or dropped by her in post-Filianore Ringed City.
RC: Shira's Armor - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Crown - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Gloves - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Trousers - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shriving Stone - wall tower, bottom floor centerIn the cylindrical building before the long stairs with many Harald Legion Knights, in the center structure on the first floor.
RC: Siegbräu - LappGiven by Lapp within the Ringed Inner Wall.
RC: Simple Gem - grave, up stairs after first dropIn Shared Grave, following the path after falling down from the crumbling stairs and continuing upward.
RC: Soul of Darkeater MidirDropped by Darkeater Midir
RC: Soul of Slave Knight GaelDropped by Slave Knight Gael
RC: Soul of a Crestfallen Knight - swamp far, behind crystalBehind a crystal structure at the far end of the muck pit, close to the building with the praying Hollow Clerics before Dragonslayer Armour.
RC: Soul of a Crestfallen Knight - swamp near left, nookIn the muck pit behind all of the Hollow Clerics near the very long ladder.
RC: Soul of a Crestfallen Knight - wall top, under dropAfter dropping down onto the side path on the right side of the Mausoleum Lookout courtyard to where the Crystal Lizard is, behind you.
RC: Soul of a Weary Warrior - lower cliff, by first alcoveIn front of the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Soul of a Weary Warrior - swamp centerIn the middle of the muck pit where the Judicator is patrolling.
RC: Soul of a Weary Warrior - swamp right, by sunken churchIn between where the Judicator patrols in the muck pit and the submerged building with the 4 Ringed Knights. Provides some shelter from his arrows.
RC: Spears of the Church - hidden boss dropDropped by Darkeater Midir
RC: Titanite Chunk - streets high, building oppositeDown a path past the room where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Titanite Chunk - streets, near left dropNear the top of the stairs by Shira, dropping down in an alcove to the left.
RC: Titanite Chunk - swamp center, peninsula edgeAlong the edge of the muck pit close to where the Judicator patrols.
RC: Titanite Chunk - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: Titanite Chunk - swamp near left, by spire topAt the edge of the muck pit, on the opposite side of the wall from the very long ladder.
RC: Titanite Chunk - swamp near right, behind rockAt the very edge of the muck pit, to the left of the submerged building with 4 Ringed Knights.
RC: Titanite Chunk - wall top, among gravesAlong the right edge of the courtyard after Mausoleum Lookout in a cluster of graves.
RC: Titanite Chunk - wall upper, courtyard alcoveIn the courtyard where the first Ringed Knight is seen, along the right wall into an alcove.
RC: Titanite Scale - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Titanite Scale - lower cliff, first alcoveIn the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Titanite Scale - lower cliff, lower pathAfter dropping down from the upper path attacked by Midir's fire-breathing to the lower path.
RC: Titanite Scale - lower cliff, path under bridgePartway down a slope to the right of the bridge where Midir first assaults you.
RC: Titanite Scale - swamp far, by minibossIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - swamp far, lagoon entranceIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - upper cliff, bridgeOn the final bridge where Midir attacks before you knock him off.
RC: Titanite Scale - wall lower, lizardDropped by the Crystal Lizard on the stairs going up from Shared Grave to Ringed Inner Wall elevator.
RC: Titanite Scale - wall top, behind spawnBehind you at the very start of the level.
RC: Titanite Slab - ashes, NPC dropGiven by Shira after defeating Midir, or dropped by her in post-Filianore Ringed City.
RC: Titanite Slab - ashes, mob dropDropped by the Ringed Knight wandering around near Gael's arena
RC: Titanite Slab - mid boss dropDropped by Halflight, Spear of the Church
RC: Twinkling Titanite - church path, left of boss doorDropping down to the left of the door leading to Halflight.
RC: Twinkling Titanite - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Twinkling Titanite - streets high, lizardDropped by the Crystal Lizard which runs across the bridge after climbing the very long ladder up from the muck pit.
RC: Twinkling Titanite - swamp near leftAt the left edge of the muck pit coming from the stairs, guarded by a Preacher Locust.
RC: Twinkling Titanite - swamp near right, on sunken churchFollowing the sloped roof of the submerged building with the 4 Ringed Knights, along the back wall
RC: Twinkling Titanite - wall top, lizard on side pathDropped by the first Crystal Lizard on the side path on the right side of the Mausoleum Lookout courtyard
RC: Twinkling Titanite - wall tower, jump from chandelierIn the cylindrical building before the long stairs with many Harald Legion Knights. Carefully drop down to the chandelier in the center, then jump to the second floor. The item is on a ledge.
RC: Violet Wrappings - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: White Birch Bow - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: White Preacher Head - swamp near, nook right of stairsPast the balcony to the right of the Ringed City Streets bonfire room entrance. Can be accessed by dropping down straight after from the bonfire, then around to the left.
RC: Wolf Ring+3 - street gardens, NPC dropDropped by Alva (invades whether embered or not, or boss defeated or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Young White Branch - swamp far left, by white tree #1Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #2Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #3Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RS: Blue Bug Pellet - broken stairs by OrbeckOn the broken stairs leading down from Orbeck's area, on the opposite side from Orbeck
RS: Blue Sentinels - HoraceGiven by Horace the Hushed by first "talking" to him, or upon death.
RS: Braille Divine Tome of Carim - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Brigand Armor - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Axe - beneath roadAt the start of the path leading down to the Madwoman in Road of Sacrifices
RS: Brigand Gauntlets - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Hood - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Trousers - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Twindaggers - beneath roadAt the end of the path guarded by the Madwoman in Road of Sacrifices
RS: Butcher Knife - NPC drop beneath roadDropped by the Butcher Knife-wielding madwoman near the start of Road of Sacrifices
RS: Chloranthy Ring+2 - road, drop across from carriageFound dropping down from the first Storyteller Corvian on the left side rather than the right side. You can then further drop down to where the madwoman is, after healing.
RS: Conjurator Boots - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Hood - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Manchettes - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Robe - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Crystal Gem - stronghold, lizardDropped by the Crystal Lizard in the building before Crystal Sage
RS: Ember - right of Halfway Fortress entranceOn the ledge with the Corvian with the Storyteller Staff, to the right of the Halfway Fortress entrance
RS: Ember - right of fire behind stronghold left roomBehind the building before Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side or go under bridge on right side
RS: Estus Shard - left of fire behind stronghold left roomBehind the building leading to Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side of go under bridge on right side
RS: Exile Greatsword - NPC drop by Farron KeepDropped by the greatsword-wielding Exile Knight before the ladder down to Farron Keep
RS: Fading Soul - woods by Crucifixion Woods bonfireDropping down from the Crucifixion Woods bonfire toward the Halfway Fortress, guarded by dogs
RS: Fallen Knight Armor - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Gauntlets - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Helm - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Trousers - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Farron Coal - keep perimeterAt the end of the Farron Keep Perimeter building on Crucifixion Woods side, behind the Black Knight
RS: Golden Falcon Shield - path from stronghold right room to Farron KeepHalfway up the stairs to the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area, go straight and follow the path down
RS: Grass Crest Shield - water by Crucifixion Woods bonfireDropping down into the crab area from Crucifixion Woods, on the other side of a tree from the greater crab
RS: Great Club - NPC drop by Farron KeepDropped by the club-wielding Exile Knight before the ladder down to Farron Keep
RS: Great Swamp Pyromancy Tome - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Great Swamp Ring - miniboss drop, by Farron KeepDropped by Greater Crab in Crucifixion Woods close to the Farron Keep outer wall
RS: Green Blossom - by deep waterIn the Crucifixion Woods crab area out in the open, close to the edge of the deep water area
RS: Green Blossom - water beneath strongholdIn the Crucifixion Woods crab area close to the Crucifixion Woods bonfire, along the left wall of the water area, to the right of the entrance to the building before Crystal Sage
RS: Heretic's Staff - stronghold left roomIn the building before Crystal Sage, entering from near Crucifixion Woods, in a corner under the first stairwell and balcony
RS: Heysel Pick - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
RS: Homeward Bone - balcony by Farron KeepAt the far end of the building where you descend into Farron Keep, by the balcony
RS: Large Soul of an Unknown Traveler - left of stairs to Farron KeepIn the area before you descend into Farron Keep, before the stairs to the far left
RS: Lingering Dragoncrest Ring+1 - waterOn a tree by the greater crab near the Crucifixion Woods bonfire, after the Grass Crest Shield tree
RS: Morne's Ring - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Ring of Sacrifice - stronghold, drop from right room balconyDrop down from the platform behind the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area
RS: Sage Ring - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sellsword Armor - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Gauntlet - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Helm - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Trousers - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Twinblades - keep perimeterIn the Farron Keep Perimeter building on Crucifixion Woods side, behind and to the right of the Black Knight
RS: Shriving Stone - road, by startDropping down to the left of the first Corvian enemy in Road of Sacrifices
RS: Sorcerer Gloves - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Hood - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Robe - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Trousers - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Soul of a Crystal SageDropped by Crystal Sage
RS: Soul of an Unknown Traveler - drop along wall from Halfway FortressFrom Halfway Fortress, hug the right wall and drop down twice on the way to the crab area
RS: Soul of an Unknown Traveler - right of door to stronghold leftOut in the open to the right of the building before Crystal Sage, as entered from Crucifixion Woods bonfire
RS: Soul of an Unknown Traveler - road, by wagonTo the right of the overturned wagon descending from the Road of Sacrifices bonfire
RS: Titanite Shard - road, on bridge after you go underCrossing the bridge you go under after the first Road of Sacrifices bonfire, after a sleeping Corvian and another Corvian guarding the pickup
RS: Titanite Shard - water by Halfway FortressDropping down into the Crucifixion Woods crab area right after Halfway Fortress, on the left wall heading toward the Black Knight building, guarded by dog
RS: Titanite Shard - woods, left of path from Halfway FortressHugging the left wall from Halfway Fortress to Crystal Sage, behind you after the first dropdown
RS: Titanite Shard - woods, surrounded by enemiesHugging the left wall from Halfway Fortress to the Crystal Sage bonfire, after a dropdown surrounded by seven Poisonhorn bugs
RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfireIn the middle of the area with the Poisonhorn bugs and Lycanthrope Hunters, following the wall where the bugs guard a Titanite Shard
RS: Xanthous Crown - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
SL: Black Iron Greatshield - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Black Knight Sword - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, guarded by a Black Knight
SL: Bloodbite Ring+1 - behind ballistaBehind the ballista, overlooking Smouldering Lake
SL: Chaos Gem - antechamber, lizard at end of long hallDropped by the Crystal Lizard found from the Antechamber bonfire, toward the Demon Cleric and to the right, then all the way down
SL: Chaos Gem - lake, far end by mobIn Smouldering Lake along the wall underneath the ballista, all the way to the left past two crabs
SL: Dragonrider Bow - by ladder from ruins basement to ballistaAfter climbing up the ladder after the Black Knight in Demon Ruins, falling back down to a ledge
SL: Ember - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Ember - ruins main lower, path to antechamberGoing down the stairs from the Antechamber bonfire, to the right, at the end of the short hallway to the next right
SL: Ember - ruins main upper, hall end by holeIn the Demon Ruins, hugging the right wall from the Demon Ruins bonfire, or making a jump from the illusory hall corridor from Antechamber bonfire
SL: Ember - ruins main upper, just after entranceBehind the first Demon Cleric from the Demon Ruins bonfire
SL: Estus Shard - antechamber, illusory wallBehind an illusory wall and Smouldering Writhing Flesh-filled corridor from Antechamber bonfire
SL: Flame Stoneplate Ring+2 - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, past the Black Knight, hidden in a corner
SL: Fume Ultra Greatsword - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Homeward Bone - path to ballistaIn the area targeted by the ballista after the long ladder guarded by the Black Knight, before the Bonewheel Skeletons
SL: Izalith Pyromancy Tome - antechamber, room near bonfireIn the room straight down from the Antechamber bonfire, past a Demon Cleric, surrounded by many Ghrus.
SL: Izalith Staff - ruins basement, second illusory wall behind chestPast an illusory wall to the left of the Large Hound Rat in Demon Ruins, and then past another illusory wall, before the basilisk area
SL: Knight Slayer's Ring - ruins basement, NPC dropDropped by Knight Slayer Tsorig after invading in the Catacombs
SL: Large Titanite Shard - lake, by entranceIn the middle of Smouldering Lake, close to the Abandoned Tomb
SL: Large Titanite Shard - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Large Titanite Shard - lake, by tree #1In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, by tree #2In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, straight from entranceIn the middle of Smouldering Lake, in between Abandoned Tomb and Demon Ruins
SL: Large Titanite Shard - ledge by Demon Ruins bonfireOn a corpse hanging off the ledge outside the Demon Ruins bonfire
SL: Large Titanite Shard - ruins basement, illusory wall in upper hallIn a chest past an illusory wall to the left of the Large Hound Rat in Demon Ruins, before the basilisk area
SL: Large Titanite Shard - side lake #1In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Large Titanite Shard - side lake #2In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Lightning Stake - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Llewellyn Shield - Horace dropDropped by Horace the Hushed upon death or quest completion.
SL: Quelana Pyromancy Tome - ruins main lower, illusory wall in grey roomAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall
SL: Sacred Flame - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Shield of Want - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Soul of a Crestfallen Knight - ruins basement, above lavaNext to the Black Knight in Demon Ruins
SL: Soul of the Old Demon KingDropped by Old Demon King in Smouldering Lake
SL: Speckled Stoneplate Ring - lake, ballista breaks bricksBehind a destructible wall in Smouldering Lake which the ballista has to destroy
SL: Titanite Chunk - path to side lake, lizardDropped by the second Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Titanite Scale - ruins basement, path to lavaIn the area with Basilisks on the way to the ballista
SL: Toxic Mist - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, in the middle of the lava pit.
SL: Twinkling Titanite - path to side lake, lizardDropped by the first Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Undead Bone Shard - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Undead Bone Shard - ruins main lower, left after stairsIn the close end of the Demon Ruins main hall, right below a Smouldering Writhing Flesh
SL: White Hair Talisman - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, at the far end of the lava pit.
SL: Yellow Bug Pellet - side lakeIn the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
UG: Ashen Estus Ring - swamp, path opposite bonfireIn the coffin similar to your initial spawn location, guarded by Corvians
UG: Black Knight Glaive - boss arenaIn the Champion Gundyr boss area
UG: Blacksmith Hammer - shrine, Andre's roomWhere Andre sits in Firelink Shrine
UG: Chaos Blade - environs, left of shrineWhere Sword Master is in Firelink Shrine
UG: Coiled Sword Fragment - shrine, dead bonfireIn the dead Firelink Shrine bonfire
UG: Ember - shopSold by Untended Graves Handmaid
UG: Eyes of a Fire Keeper - shrine, Irina's roomBehind an illusory wall, in the same location Irina sits in Firelink Shrine
UG: Hidden Blessing - cemetery, behind coffinBehind the coffin that had a Titanite Shard in Cemetery of Ash
UG: Hornet Ring - environs, right of main path after killing FK bossOn a cliffside to the right of the main path leading up to dark Firelink Shrine, after Abyss Watchers is defeated.
UG: Life Ring+3 - shrine, behind big throneBehind Prince Lothric's throne
UG: Priestess Ring - shopSold or dropped by Untended Graves Handmaid. Killing her is not recommended
UG: Ring of Steel Protection+1 - environs, behind bell towerBehind Bell Tower to the right
UG: Shriving Stone - swamp, by bonfireAt the very start of the area
UG: Soul of Champion GundyrDropped by Champion Gundyr
UG: Soul of a Crestfallen Knight - environs, above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
UG: Soul of a Crestfallen Knight - swamp, centerClose to where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, left path by fountainIn a path to the left of where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, right path by fountainIn a path to the right of where Ashen Estus Flask was in Cemetery of Ash
UG: Wolf Knight Armor - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Gauntlets - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Helm - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Leggings - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
US: Alluring Skull - foot, behind carriageGuarded by two dogs after the Foot of the High Wall bonfire
US: Alluring Skull - on the way to tower, behind buildingAfter the ravine bridge leading to Eygon and the Giant's tower, wrapping around the building to the right.
US: Alluring Skull - tower village building, upstairsUp the stairs of the building with Cage Spiders after the Fire Demon, before the dogs
US: Bloodbite Ring - miniboss in sewerDropped by the large rat in the sewers with grave access
US: Blue Wooden Shield - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Caduceus Round Shield - right after stable exitAfter exiting the building across the bridge to the right of the first Undead Settlement building, to the left
US: Caestus - sewerIn the tunnel with the Giant Hound Rat and Grave Key door, from the ravine bridge toward Dilapidated Bridge bonfire
US: Charcoal Pine Bundle - first building, bottom floorDown the stairs in the first building
US: Charcoal Pine Bundle - first building, middle floorOn the bottom floor of the first building
US: Charcoal Pine Resin - hanging corpse roomIn the building after the burning tree and Cathedral Evangelist, in the room with the many hanging corpses
US: Chloranthy Ring - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Cleric Blue Robe - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Gloves - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Hat - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Trousers - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cornyx's Garb - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Garb - kill CornyxDropped by Cornyx
US: Cornyx's Skirt - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Skirt - kill CornyxDropped by Cornyx
US: Cornyx's Wrap - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Wrap - kill CornyxDropped by Cornyx
US: Covetous Silver Serpent Ring+2 - tower village, drop down from roofAt the back of a roof near the end of the Fire Demon loop, dropping down past where Flynn's Ring is
US: Ember - behind burning treeBehind the burning tree with the Cathedral Evangelist
US: Ember - bridge on the way to towerOn the ravine bridge leading toward Eygon and the Giant's tower
US: Ember - by stairs to bossNext to the stairs leading up to Curse-Rotted Greatwood fight, near a tree guarded by a dog
US: Ember - by white treeNear the Birch Tree where giant shoots arrows
US: Ember - tower basement, minibossIn the room with the Outrider Knight
US: Estus Shard - under burning treeIn front of the burning tree guarded by the Cathedral Evangelist
US: Fading Soul - by white treeNear the Birch Tree where giant shoots arrows
US: Fading Soul - outside stableIn the thrall area to the right of the bridge to the right of the burning tree with the Cathedral Evangelist
US: Fire Clutch Ring - wooden walkway past stableFrom the area bombarded by firebombs above the Cliff Underside bonfire
US: Fire Gem - tower village, miniboss dropDropped by the Fire Demon you fight with Siegward
US: Firebomb - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a rooftop overlooking the Cliff Underside area.
US: Flame Stoneplate Ring - hanging corpse by Mound-Maker transportOn a hanging corpse in the area with the Pit of Hollows cage manservant, after the thrall area, overlooking the entrance to the Giant's tower.
US: Flynn's Ring - tower village, rooftopOn the roof toward the end of the Fire Demon loop, past the Cathedral Evangelists
US: Great Scythe - building by white tree, balconyOn the balcony of the building before Curse-Rotted Greatwood, coming from Dilapidated Bridge bonfire
US: Hand Axe - by CornyxNext to Cornyx's cell
US: Hawk Ring - Giant ArcherDropped by Giant, either by killing him or collecting all of the birch tree items locations in the base game.
US: Heavy Gem - HawkwoodGiven or dropped by Hawkwood after defeating Curse-Rotted Greatwood or Crystal Sage
US: Heavy Gem - chasm, lizardDrop by Crystal Lizard in ravine accessible by Grave Key or dropping down near Eygon.
US: Homeward Bone - foot, drop overlookUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Homeward Bone - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a roof overlooking the ravine bridge.
US: Homeward Bone - tower village, jump from roofAt the end of the loop from the Siegward Demon fight, after dropping down from the roof onto the tower with Chloranthy Ring, to the right of the tower entrance
US: Homeward Bone - tower village, right at startUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Human Pine Resin - tower village building, chest upstairsIn a chest after Fire Demon. Cage Spiders activate open opening it.
US: Irithyll Straight Sword - miniboss drop, by Road of SacrificesDropped by the Boreal Outright Knight before Road of Sacrifices
US: Kukri - hanging corpse above burning treeHanging corpse high above the burning tree with the Cathedral Evangelist. Must be shot down with an arrow or projective.
US: Large Club - tower village, by minibossIn the Fire Demon area
US: Large Soul of a Deserted Corpse - across from Foot of the High WallOn the opposite tower from the Foot of the High Wall bonfire
US: Large Soul of a Deserted Corpse - around corner by Cliff UndersideAfter going up the stairs from Curse-Rotted Greatwood to Cliff Underside area, on a cliff edge to the right
US: Large Soul of a Deserted Corpse - by white treeNear the Birch Tree where giant shoots arrows
US: Large Soul of a Deserted Corpse - hanging corpse room, over stairsOn a hanging corpse in the building after the burning tree. Can be knocked down by dropping onto the stairs through the broken railing.
US: Large Soul of a Deserted Corpse - on the way to tower, by wellAfter the ravine bridge leading toward Eygon and the Giant's tower, next to the well to the right
US: Large Soul of a Deserted Corpse - stableIn the building with stables across the bridge and to the right from the first Undead Settlement building
US: Life Ring+1 - tower on the way to villageOn the wooden rafters near where Siegward is waiting for Fire Demon
US: Loincloth - by Velka statueNext to the Velka statue. Requires Grave Key or dropping down near Eygon and backtracking through the skeleton area.
US: Loretta's Bone - first building, hanging corpse on balconyOn a hanging corpse after the first building, can be knocked down by rolling into it
US: Mirrah Gloves - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Trousers - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Vest - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mortician's Ashes - graveyard by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, at the close end of the graveyard
US: Mound-makers - HodrickGiven by Hodrick if accessing the Pit of Hollows before fighting Curse-Rotted Greatwood, or dropped after invading him with Sirris.
US: Northern Armor - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Gloves - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Helm - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Trousers - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Old Sage's Blindfold - kill CornyxDropped by Cornyx
US: Pale Tongue - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Partizan - hanging corpse above Cliff UndersideOn a hanging corpse on the path from Cliff Underside to Cornyx's cage. Must be shot down with an arrow or projective.
US: Plank Shield - outside stable, by NPCIn the thrall area across the bridge from the first Undead Settlement building, on a cliff edge overlooking the ravine bridge.
US: Poisonbite Ring+1 - graveyard by white tree, near wellBehind the well in the back of area where the Giant shoots arrows, nearby where the flamberge-wielding thrall drops down.
US: Pyromancy Flame - CornyxGiven by Cornyx in Firelink Shrine or dropped.
US: Red Bug Pellet - tower village building, basementOn the floor of the building after the Fire Demon encounter
US: Red Hilted Halberd - chasm cryptIn the skeleton area accessible from Grave Key or dropping down from near Eygon
US: Red and White Shield - chasm, hanging corpseOn a hanging corpse in the ravine accessible with the Grave Key or dropping down near Eygon, to the entrance of Irina's prison. Must be shot down with an arrow or projective.
US: Reinforced Club - by white treeNear the Birch Tree where giant shoots arrows
US: Repair Powder - first building, balconyOn the balcony of the first Undead Settlement building
US: Rusted Coin - awning above Dilapidated BridgeOn a wooden ledge near the Dilapidated Bridge bonfire. Must be jumped to from near Cathedral Evangelist enemy
US: Saint's Talisman - chasm, by ladderFrom the ravine accessible via Grave Key or dropping near Eygon, before ladder leading up to Irina of Carim
US: Sharp Gem - lizard by Dilapidated BridgeDrop by Crystal Lizard near Dilapidated Bridge bonfire.
US: Siegbräu - SiegwardGiven by Siegward after helping him defeat the Fire Demon.
US: Small Leather Shield - first building, hanging corpse by entranceHanging corpse in the first building, to the right of the entrance
US: Soul of a Nameless Soldier - top of towerAt the top of the tower where Giant shoots arrows
US: Soul of an Unknown Traveler - back alley, past cratesAfter exiting the building after the burning tree on the way to the Dilapidated Bridge bonfire. Hidden behind some crates between two buildings on the right.
US: Soul of an Unknown Traveler - chasm cryptIn the skeleton area accessible Grave Key or dropping down from near Eygon
US: Soul of an Unknown Traveler - pillory past stableIn the area bombarded by firebombs above the Cliff Underside bonfire
US: Soul of an Unknown Traveler - portcullis by burning treeBehind a grate to the left of the burning tree and Cathedral Evangelist
US: Soul of the Rotted GreatwoodDropped by Curse Rotted Greatwood
US: Spotted Whip - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Sunset Armor - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Gauntlets - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Helm - Pit of Hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Leggings - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Titanite Shard - back alley, side pathOn a side path to the right of the Cathedral Evangelist before the Dilapidated Bridge bonfire
US: Titanite Shard - back alley, up ladderNext to the Cathedral Evangelist close to the Dilapidated Bridge bonfire
US: Titanite Shard - chasm #1In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - chasm #2In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - lower path to Cliff UndersideAt the end of the cliffside path next to Cliff Underside bonfire, guarded by a Hollow Peasant wielding a four-pronged plow.
US: Titanite Shard - porch after burning treeIn front of the building after the burning tree and Cathedral Evangelist
US: Tower Key - kill IrinaDropped by Irina of Carim
US: Transposing Kiln - boss dropDropped by Curse Rotted Greatwood
US: Undead Bone Shard - by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, jumping to the floating platform on the right
US: Wargod Wooden Shield - Pit of HollowsIn the Pit of Hollows
US: Warrior of Sunlight - hanging corpse room, drop through holeDropping through a hole in the floor in the first building after the burning tree.
US: Whip - back alley, behind wooden wallIn one of the houses between building after the burning tree and the Dilapidated Bridge bonfire
US: Young White Branch - by white tree #1Near the Birch Tree where giant shoots arrows
US: Young White Branch - by white tree #2Near the Birch Tree where giant shoots arrows
+ diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 61215dbc6043..484afdce3fcb 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -3,52 +3,73 @@ ## 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 -- [Dark Souls III Maptracker Pack](https://github.com/Br00ty/DS3_AP_Maptracker/releases/latest), for use with [Poptracker](https://github.com/black-sliver/PopTracker/releases) +- Map tracker not yet updated for 3.0.0 -## General Concept +## Setting Up - -**This mod can ban you permanently from the FromSoftware servers if used online.** - -The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command -prompt where you can read information about your run and write any command to interact with the Archipelago server. +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. -This client has only been tested with the Official Steam version of the game at version 1.15. It does not matter which DLCs are installed. However, you will have to downpatch your Dark Souls III installation from current patch. +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 +downpatched your game to use an older version of the randomizer, you'll need to reinstall the latest +version before using this version. You should also delete the `dinput8.dll` file if you still have +one from an older randomizer version. -## Downpatching Dark Souls III +### One-Time Setup -To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database. +Before you first connect to a multiworld, you need to generate the local data files for your world's +randomized item and (optionally) enemy locations. You only need to do this once per multiworld. -1. Launch Steam (in online mode). -2. Press the Windows Key + R. This will open the Run window. -3. Open the Steam console by typing the following string: `steam://open/console`. Steam should now open in Console Mode. -4. Insert the string of the depot you wish to download. For the AP-supported v1.15, you will want to use: `download_depot 374320 374321 4471176929659548333`. -5. Steam will now download the depot. Note: There is no progress bar for the download in Steam, but it is still downloading in the background. -6. Back up your existing game executable (`DarkSoulsIII.exe`) found in `\Steam\steamapps\common\DARK SOULS III\Game`. Easiest way to do this is to move it to another directory. If you have file extensions enabled, you can instead rename the executable to `DarkSoulsIII.exe.bak`. -7. Return to the Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like `\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX`. -8. Take the `DarkSoulsIII.exe` from that folder and place it in `\Steam\steamapps\common\DARK SOULS III\Game`. -9. Back up and delete your save file (`DS30000.sl2`) in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type `%appdata%` and hit enter. Alternatively: open File Explorer > View > Hidden Items and follow `C:\Users\\AppData\Roaming\DarkSoulsIII\`. -10. If you did all these steps correctly, you should be able to confirm your game version in the upper-left corner after launching Dark Souls III. +1. Before you first connect to a multiworld, run `randomizer\DS3Randomizer.exe`. +2. Put in your Archipelago room address (usually something like `archipelago.gg:12345`), your player + name (also known as your "slot name"), and your password if you have one. -## Installing the Archipelago mod +3. Click "Load" and wait a minute or two. -Get the `dinput8.dll` from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and -add it at the root folder of your game (e.g. `SteamLibrary\steamapps\common\DARK SOULS III\Game`) +### Running and Connecting the Game -## Joining a MultiWorld Game +To run _Dark Souls III_ in Archipelago mode: -1. Run Steam in offline mode to avoid being banned. -2. Launch Dark Souls III. -3. Type in `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME} password:{PASSWORD}` in the "Windows Command Prompt" that opened. For example: `/connect archipelago.gg:38281 "Example Name" password:"Example Password"`. The password parameter is only necessary if your game requires one. -4. Once connected, create a new game, choose a class and wait for the others before starting. -5. You can quit and launch at anytime during a game. +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. -## Where do I get a config file? +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. + +3. Type `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}` into the command prompt, with the + appropriate values filled in. For example: `/connect archipelago.gg:24242 PlayerName`. + +4. Start playing as normal. An "Archipelago connected" message will appear onscreen once you have + control of your character and the connection is established. + +## Frequently Asked Questions + +### 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/dark_souls_3/test/TestDarkSouls3.py b/worlds/dark_souls_3/test/TestDarkSouls3.py new file mode 100644 index 000000000000..7acdad465da7 --- /dev/null +++ b/worlds/dark_souls_3/test/TestDarkSouls3.py @@ -0,0 +1,27 @@ +from test.bases import WorldTestBase + +from worlds.dark_souls_3.Items import item_dictionary +from worlds.dark_souls_3.Locations import location_tables +from worlds.dark_souls_3.Bosses import all_bosses + +class DarkSouls3Test(WorldTestBase): + game = "Dark Souls III" + + def testLocationDefaultItems(self): + for locations in location_tables.values(): + for location in locations: + if location.default_item_name: + self.assertIn(location.default_item_name, item_dictionary) + + def testLocationsUnique(self): + names = set() + for locations in location_tables.values(): + for location in locations: + self.assertNotIn(location.name, names) + names.add(location.name) + + def testBossLocations(self): + all_locations = {location.name for locations in location_tables.values() for location in locations} + for boss in all_bosses: + for location in boss.locations: + self.assertIn(location, all_locations) diff --git a/worlds/kdl3/Names/__init__.py b/worlds/dark_souls_3/test/__init__.py similarity index 100% rename from worlds/kdl3/Names/__init__.py rename to worlds/dark_souls_3/test/__init__.py diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index a9dfcc5044b1..37eae9b447d1 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,11 +8,15 @@ from .Options import DLCQuestOptions from .Regions import create_regions from .Rules import set_rules +from .presets import dlcq_options_presets +from .option_groups import dlcq_option_groups client_version = 0 class DLCqwebworld(WebWorld): + options_presets = dlcq_options_presets + option_groups = dlcq_option_groups setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Archipelago DLCQuest game on your computer.", @@ -68,8 +72,16 @@ def create_items(self): self.multiworld.itempool += created_items - if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both: - self.multiworld.early_items[self.player]["Movement Pack"] = 1 + campaign = self.options.campaign + has_both = campaign == Options.Campaign.option_both + has_base = campaign == Options.Campaign.option_basic or has_both + has_big_bundles = self.options.coinsanity and self.options.coinbundlequantity > 50 + early_items = self.multiworld.early_items + if has_base: + if has_both and has_big_bundles: + early_items[self.player]["Incredibly Important Pack"] = 1 + else: + early_items[self.player]["Movement Pack"] = 1 for item in items_to_exclude: if item in self.multiworld.itempool: @@ -78,7 +90,7 @@ def create_items(self): def precollect_coinsanity(self): if self.options.campaign == Options.Campaign.option_basic: if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5: - self.multiworld.push_precollected(self.create_item("Movement Pack")) + self.multiworld.push_precollected(self.create_item("DLC Quest: Coin Bundle")) def create_item(self, item: Union[str, ItemData], classification: ItemClassification = None) -> DLCQuestItem: if isinstance(item, str): diff --git a/worlds/dlcquest/option_groups.py b/worlds/dlcquest/option_groups.py new file mode 100644 index 000000000000..9510c061e18f --- /dev/null +++ b/worlds/dlcquest/option_groups.py @@ -0,0 +1,27 @@ +from typing import List + +from Options import ProgressionBalancing, Accessibility, OptionGroup +from .Options import (Campaign, ItemShuffle, TimeIsMoney, EndingChoice, PermanentCoins, DoubleJumpGlitch, CoinSanity, + CoinSanityRange, DeathLink) + +dlcq_option_groups: List[OptionGroup] = [ + OptionGroup("General", [ + Campaign, + ItemShuffle, + CoinSanity, + ]), + OptionGroup("Customization", [ + EndingChoice, + PermanentCoins, + CoinSanityRange, + ]), + OptionGroup("Tedious and Grind", [ + TimeIsMoney, + DoubleJumpGlitch, + ]), + OptionGroup("Advanced Options", [ + DeathLink, + ProgressionBalancing, + Accessibility, + ]), +] diff --git a/worlds/dlcquest/presets.py b/worlds/dlcquest/presets.py new file mode 100644 index 000000000000..ccfd79399521 --- /dev/null +++ b/worlds/dlcquest/presets.py @@ -0,0 +1,68 @@ +from typing import Any, Dict + +from .Options import DoubleJumpGlitch, CoinSanity, CoinSanityRange, PermanentCoins, TimeIsMoney, EndingChoice, Campaign, ItemShuffle + +all_random_settings = { + DoubleJumpGlitch.internal_name: "random", + CoinSanity.internal_name: "random", + CoinSanityRange.internal_name: "random", + PermanentCoins.internal_name: "random", + TimeIsMoney.internal_name: "random", + EndingChoice.internal_name: "random", + Campaign.internal_name: "random", + ItemShuffle.internal_name: "random", + "death_link": "random", +} + +main_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_basic, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +lfod_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_live_freemium_or_die, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +easy_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_none, + CoinSanityRange.internal_name: 40, + PermanentCoins.internal_name: PermanentCoins.option_true, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +hard_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_simple, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_optional, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + + +dlcq_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Main campaign": main_campaign_settings, + "LFOD campaign": lfod_campaign_settings, + "Both easy": easy_settings, + "Both hard": hard_settings, +} diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py index 3e9acac7e791..c6c594b6a004 100644 --- a/worlds/dlcquest/test/TestOptionsLong.py +++ b/worlds/dlcquest/test/TestOptionsLong.py @@ -5,7 +5,6 @@ from .option_names import options_to_include from .checks.world_checks import assert_can_win, assert_same_number_items_locations from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld -from ... import AutoWorldRegister def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld): diff --git a/worlds/dlcquest/test/__init__.py b/worlds/dlcquest/test/__init__.py index 8a39b43a2cfd..0432ae8b60ba 100644 --- a/worlds/dlcquest/test/__init__.py +++ b/worlds/dlcquest/test/__init__.py @@ -4,7 +4,7 @@ from argparse import Namespace from BaseClasses import MultiWorld -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from .. import DLCqworld from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from worlds.AutoWorld import call_all diff --git a/worlds/dlcquest/test/checks/world_checks.py b/worlds/dlcquest/test/checks/world_checks.py index cc2fa7f51ad2..48c919c0f62b 100644 --- a/worlds/dlcquest/test/checks/world_checks.py +++ b/worlds/dlcquest/test/checks/world_checks.py @@ -1,6 +1,6 @@ from typing import List -from BaseClasses import MultiWorld, ItemClassification +from BaseClasses import MultiWorld from .. import DLCQuestTestBase from ... import Options @@ -14,7 +14,7 @@ def get_all_location_names(multiworld: MultiWorld) -> List[str]: def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): - campaign = multiworld.campaign[1] + campaign = multiworld.worlds[1].options.campaign all_items = [item.name for item in multiworld.get_items()] if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: tester.assertIn("Victory Basic", all_items) @@ -25,7 +25,7 @@ def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): for item in multiworld.get_items(): multiworld.state.collect(item) - campaign = multiworld.campaign[1] + campaign = multiworld.worlds[1].options.campaign if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state)) if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both: @@ -39,4 +39,4 @@ def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld): non_event_locations = [location for location in multiworld.get_locations() if not location.advancement] - tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) \ No newline at end of file + tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) 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/Options.py b/worlds/doom_1993/Options.py index f65952d3eb49..c9c61110328c 100644 --- a/worlds/doom_1993/Options.py +++ b/worlds/doom_1993/Options.py @@ -16,9 +16,9 @@ class Goal(Choice): class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. + Choose the game difficulty. These options match DOOM's skill levels. + baby (I'm too young to die.) Same as easy, with double ammo pickups and half damage taken. + easy (Hey, not too rough.) Less monsters or strength. medium (Hurt me plenty.) Default. hard (Ultra-Violence.) More monsters or strength. nightmare (Nightmare!) Monsters attack more rapidly and respawn. @@ -29,6 +29,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_nightmare = 4 + alias_itytd = 0 + alias_hntr = 1 + alias_hmp = 2 + alias_uv = 3 + alias_nm = 4 default = 2 @@ -112,7 +117,7 @@ class StartWithComputerAreaMaps(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_name="Reset Level on Death" + display_name = "Reset Level on Death" class Episode1(DefaultOnToggle): 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_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md index 8906efac9cea..85061609abbb 100644 --- a/worlds/doom_1993/docs/setup_en.md +++ b/worlds/doom_1993/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM_1993/) +- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software @@ -17,7 +17,7 @@ You can find the folder in steam by finding the game in your library, right-clicking it and choosing **Manage -> Browse Local Files**. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Select `Ultimate DOOM` from the drop-down @@ -28,6 +28,24 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apdoom -game doom -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apfliplevels 0` will disable flipping levels. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (I'm too young to die) to 5 (Nightmare!) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. 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/doom_ii/Options.py b/worlds/doom_ii/Options.py index cc39512a176e..98c8ebc56e16 100644 --- a/worlds/doom_ii/Options.py +++ b/worlds/doom_ii/Options.py @@ -6,9 +6,9 @@ class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. + Choose the game difficulty. These options match DOOM's skill levels. + baby (I'm too young to die.) Same as easy, with double ammo pickups and half damage taken. + easy (Hey, not too rough.) Less monsters or strength. medium (Hurt me plenty.) Default. hard (Ultra-Violence.) More monsters or strength. nightmare (Nightmare!) Monsters attack more rapidly and respawn. @@ -19,6 +19,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_nightmare = 4 + alias_itytd = 0 + alias_hntr = 1 + alias_hmp = 2 + alias_uv = 3 + alias_nm = 4 default = 2 @@ -102,7 +107,7 @@ class StartWithComputerAreaMaps(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_message="Reset level on death" + display_name = "Reset Level on Death" class Episode1(DefaultOnToggle): diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md index 87054ab30783..e444f85bd7c7 100644 --- a/worlds/doom_ii/docs/setup_en.md +++ b/worlds/doom_ii/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/) +- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software @@ -15,7 +15,7 @@ You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Select `DOOM II` from the drop-down @@ -26,6 +26,24 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apdoom -game doom2 -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apfliplevels 0` will disable flipping levels. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (I'm too young to die) to 5 (Nightmare!) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index 23dfa0633eb4..3c35c4cb0986 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -304,13 +304,13 @@ def queuer(): async def factorio_server_watcher(ctx: FactorioContext): - savegame_name = os.path.abspath(ctx.savegame_name) + savegame_name = os.path.abspath(os.path.join(ctx.write_data_path, "saves", "Archipelago", ctx.savegame_name)) if not os.path.exists(savegame_name): logger.info(f"Creating savegame {savegame_name}") subprocess.run(( executable, "--create", savegame_name, "--preset", "archipelago" )) - factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name, + factorio_process = subprocess.Popen((executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)), stderr=subprocess.PIPE, stdout=subprocess.PIPE, @@ -331,7 +331,8 @@ async def factorio_server_watcher(ctx: FactorioContext): factorio_queue.task_done() if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: - ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) + ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password, + timeout=5) if not ctx.server: logger.info("Established bridge to Factorio Server. " "Ready to connect to Archipelago via /connect") @@ -405,8 +406,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient): info = json.loads(rcon_client.send_command("/ap-rcon-info")) ctx.auth = info["slot_name"] ctx.seed_name = info["seed_name"] - # 0.2.0 addition, not present earlier - death_link = bool(info.get("death_link", False)) + death_link = info["death_link"] ctx.energy_link_increment = info.get("energy_link", 0) logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}") if ctx.energy_link_increment and ctx.ui: diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index d7b3d4b1ebca..7dee04afbee3 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 @@ -34,9 +35,11 @@ "author": "Berserker", "homepage": "https://archipelago.gg", "description": "Integration client for the Archipelago Randomizer", - "factorio_version": "1.1", + "factorio_version": "2.0", "dependencies": [ - "base >= 1.1.0", + "base >= 2.0.15", + "? quality >= 2.0.15", + "! space-age", "? science-not-invited", "? factory-levels" ] @@ -88,6 +91,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 +115,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 +132,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, - "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), + "recipes": recipes, + "random": random, + "flop_random": flop_random, + "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}, + "free_sample_quality_name": world.options.free_samples_quality.current_key, "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, "liquids": fluids, - "goal": multiworld.goal[player].value, - "energy_link": multiworld.energy_link[player].value, - "useless_technologies": useless_technologies, - "chunk_shuffle": multiworld.chunk_shuffle[player].value if hasattr(multiworld, "chunk_shuffle") else 0, + "removed_technologies": world.removed_technologies, + "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..72f438778b60 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,11 +1,13 @@ from __future__ import annotations + +from dataclasses import dataclass import typing -import datetime -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, DefaultOnToggle, Range, DeathLink, Toggle, \ + StartInventoryPool, PerGameCommonOptions, OptionGroup + # 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))) @@ -119,6 +121,18 @@ class FreeSamples(Choice): default = 3 +class FreeSamplesQuality(Choice): + """If free samples are on, determine the quality of the granted items. + Requires the quality mod, which is part of the Space Age DLC. Without it, normal quality is given.""" + display_name = "Free Samples Quality" + option_normal = 0 + option_uncommon = 1 + option_rare = 2 + option_epic = 3 + option_legendary = 4 + default = 0 + + class TechTreeLayout(Choice): """Selects how the tech tree nodes are interwoven. Single: No dependencies @@ -258,6 +272,12 @@ class AtomicRocketTrapCount(TrapCount): display_name = "Atomic Rocket Traps" +class AtomicCliffRemoverTrapCount(TrapCount): + """Trap items that when received trigger an atomic rocket explosion on a random cliff. + Warning: there is no warning. The launch is instantaneous.""" + display_name = "Atomic Cliff Remover Traps" + + class EvolutionTrapCount(TrapCount): """Trap items that when received increase the enemy evolution.""" display_name = "Evolution Traps" @@ -279,19 +299,23 @@ class FactorioWorldGen(OptionDict): with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings""" display_name = "World Generation" # FIXME: do we want default be a rando-optimized default or in-game DS? - value: typing.Dict[str, typing.Dict[str, typing.Any]] + value: dict[str, dict[str, typing.Any]] default = { - "terrain_segmentation": 0.5, - "water": 1.5, "autoplace_controls": { + # terrain + "water": {"frequency": 1, "size": 1, "richness": 1}, + "nauvis_cliff": {"frequency": 1, "size": 1, "richness": 1}, + "starting_area_moisture": {"frequency": 1, "size": 1, "richness": 1}, + # resources "coal": {"frequency": 1, "size": 3, "richness": 6}, "copper-ore": {"frequency": 1, "size": 3, "richness": 6}, "crude-oil": {"frequency": 1, "size": 3, "richness": 6}, - "enemy-base": {"frequency": 1, "size": 1, "richness": 1}, "iron-ore": {"frequency": 1, "size": 3, "richness": 6}, "stone": {"frequency": 1, "size": 3, "richness": 6}, + "uranium-ore": {"frequency": 1, "size": 3, "richness": 6}, + # misc "trees": {"frequency": 1, "size": 1, "richness": 1}, - "uranium-ore": {"frequency": 1, "size": 3, "richness": 6} + "enemy-base": {"frequency": 1, "size": 1, "richness": 1}, }, "seed": None, "starting_area": 1, @@ -333,8 +357,6 @@ class FactorioWorldGen(OptionDict): } schema = Schema({ "basic": { - Optional("terrain_segmentation"): FloatRange(0.166, 6), - Optional("water"): FloatRange(0.166, 6), Optional("autoplace_controls"): { str: { "frequency": FloatRange(0, 6), @@ -386,7 +408,7 @@ class FactorioWorldGen(OptionDict): } }) - def __init__(self, value: typing.Dict[str, typing.Any]): + def __init__(self, value: dict[str, typing.Any]): advanced = {"pollution", "enemy_evolution", "enemy_expansion"} self.value = { "basic": {k: v for k, v in value.items() if k not in advanced}, @@ -405,7 +427,7 @@ def optional_min_lte_max(container, min_key, max_key): optional_min_lte_max(enemy_expansion, "min_expansion_cooldown", "max_expansion_cooldown") @classmethod - def from_any(cls, data: typing.Dict[str, typing.Any]) -> FactorioWorldGen: + def from_any(cls, data: dict[str, typing.Any]) -> FactorioWorldGen: if type(data) == dict: return cls(data) else: @@ -419,53 +441,74 @@ class ImportedBlueprint(DefaultOnToggle): class EnergyLink(Toggle): """Allow sending energy to other worlds. 25% of the energy is lost in the transfer.""" - 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 + display_name = "Energy Link" + + +@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 + free_samples_quality: FreeSamplesQuality + 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 + atomic_cliff_remover_traps: AtomicCliffRemoverTrapCount + attack_traps: AttackTrapCount + evolution_traps: EvolutionTrapCount + evolution_trap_increase: EvolutionTrapIncrease + death_link: DeathLink + energy_link: EnergyLink + start_inventory_from_pool: StartInventoryPool + + +option_groups: list[OptionGroup] = [ + OptionGroup( + "Technologies", + [ + TechTreeLayout, + Progressive, + MinTechCost, + MaxTechCost, + TechCostDistribution, + TechCostMix, + RampingTechCosts, + TechTreeInformation, + ] + ), + OptionGroup( + "Traps", + [ + AttackTrapCount, + EvolutionTrapCount, + EvolutionTrapIncrease, + TeleportTrapCount, + GrenadeTrapCount, + ClusterGrenadeTrapCount, + ArtilleryTrapCount, + AtomicRocketTrapCount, + AtomicCliffRemoverTrapCount, + ], + start_collapsed=True + ), +] 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..6111462e8ca9 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,24 +1,23 @@ from __future__ import annotations -import orjson -import logging -import os -import string +import functools import pkgutil +import string from collections import Counter from concurrent.futures import ThreadPoolExecutor -from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any +from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any, Optional + +import orjson import Utils 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")) @@ -33,8 +32,23 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: tech_table: Dict[str, int] = {} technology_table: Dict[str, Technology] = {} +start_unlocked_recipes = { + "offshore-pump", + "boiler", + "steam-engine", + "automation-science-pack", + "inserter", + "small-electric-pole", + "copper-cable", + "lab", + "electronic-circuit", + "electric-mining-drill", + "pipe", + "pipe-to-ground", +} + -def always(state): +def always(state) -> bool: return True @@ -51,15 +65,13 @@ def __hash__(self): class Technology(FactorioElement): # maybe make subclass of Location? has_modifier: bool factorio_id: int - ingredients: Set[str] progressive: Tuple[str] unlocks: Union[Set[str], bool] # bool case is for progressive technologies - def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = (), + def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (), has_modifier: bool = False, unlocks: Union[Set[str], bool] = None): - self.name = name + self.name = technology_name self.factorio_id = factorio_id - self.ingredients = ingredients self.progressive = progressive self.has_modifier = has_modifier if unlocks: @@ -67,19 +79,6 @@ def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressi else: self.unlocks = set() - def build_rule(self, player: int): - logging.debug(f"Building rules for {self.name}") - - return lambda state: all(state.has(f"Automated {ingredient}", player) - for ingredient in self.ingredients) - - def get_prior_technologies(self) -> Set[Technology]: - """Get Technologies that have to precede this one to resolve tree connections.""" - technologies = set() - for ingredient in self.ingredients: - technologies |= required_technologies[ingredient] # technologies that unlock the recipes - return technologies - def __hash__(self): return self.factorio_id @@ -92,22 +91,22 @@ def useful(self) -> bool: class CustomTechnology(Technology): """A particularly configured Technology for a world.""" + ingredients: Set[str] def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int): - ingredients = origin.ingredients & allowed_packs - military_allowed = "military-science-pack" in allowed_packs \ - and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) - or origin.name == "rocket-silo") + ingredients = allowed_packs self.player = player - if origin.name not in world.worlds[player].special_nodes: - if military_allowed: - ingredients.add("military-science-pack") - ingredients = list(ingredients) - ingredients.sort() # deterministic sample - ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) - elif origin.name == "rocket-silo" and military_allowed: - ingredients.add("military-science-pack") - super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id) + if origin.name not in world.special_nodes: + ingredients = set(world.random.sample(list(ingredients), world.random.randint(1, len(ingredients)))) + self.ingredients = ingredients + super(CustomTechnology, self).__init__(origin.name, origin.factorio_id) + + def get_prior_technologies(self) -> Set[Technology]: + """Get Technologies that have to precede this one to resolve tree connections.""" + technologies = set() + for ingredient in self.ingredients: + technologies |= required_technologies[ingredient] # technologies that unlock the recipes + return technologies class Recipe(FactorioElement): @@ -150,19 +149,22 @@ def rel_cost(self) -> float: ingredients = sum(self.ingredients.values()) return min(ingredients / amount for product, amount in self.products.items()) - @property + @functools.cached_property def base_cost(self) -> Dict[str, int]: ingredients = Counter() - for ingredient, cost in self.ingredients.items(): - if ingredient in all_product_sources: - for recipe in all_product_sources[ingredient]: - if recipe.ingredients: - ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in - recipe.base_cost.items()}) - else: - ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient] - else: - ingredients[ingredient] += cost + try: + for ingredient, cost in self.ingredients.items(): + if ingredient in all_product_sources: + for recipe in all_product_sources[ingredient]: + if recipe.ingredients: + ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in + recipe.base_cost.items()}) + else: + ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient] + else: + ingredients[ingredient] += cost + except RecursionError as e: + raise Exception(f"Infinite recursion in ingredients of {self}.") from e return ingredients @property @@ -192,9 +194,12 @@ def __init__(self, name, categories): # recipes and technologies can share names in Factorio for technology_name, data in sorted(techs_future.result().items()): - current_ingredients = set(data["ingredients"]) - technology = Technology(technology_name, current_ingredients, factorio_tech_id, - has_modifier=data["has_modifier"], unlocks=set(data["unlocks"])) + technology = Technology( + technology_name, + factorio_tech_id, + has_modifier=data["has_modifier"], + unlocks=set(data["unlocks"]) - start_unlocked_recipes, + ) factorio_tech_id += 1 tech_table[technology_name] = technology.factorio_id technology_table[technology_name] = technology @@ -227,11 +232,12 @@ def __init__(self, name, categories): recipes[recipe_name] = recipe if set(recipe.products).isdisjoint( # prevents loop recipes like uranium centrifuging - set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \ + set(recipe.ingredients)) and ("barrel" not in recipe.products or recipe.name == "barrel") and \ not recipe_name.endswith("-reprocessing"): for product_name in recipe.products: all_product_sources.setdefault(product_name, set()).add(recipe) +assert all(recipe_name in raw_recipes for recipe_name in start_unlocked_recipes), "Unknown Recipe defined." machines: Dict[str, Machine] = {} @@ -249,9 +255,7 @@ def __init__(self, name, categories): # build requirements graph for all technology ingredients -all_ingredient_names: Set[str] = set() -for technology in technology_table.values(): - all_ingredient_names |= technology.ingredients +all_ingredient_names: Set[str] = set(Options.MaxSciencePack.get_ordered_science_packs()) def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]: @@ -320,13 +324,17 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) -def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_recipe: Recipe) -> Set[str]: +def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Recipe, + satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]: techs = set() if silo_recipe: for ingredient in silo_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) for ingredient in part_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) + if cargo_landing_pad_recipe: + for ingredient in cargo_landing_pad_recipe.ingredients: + techs |= recursively_get_unlocking_technologies(ingredient) if satellite_recipe: techs |= satellite_recipe.unlocking_technologies for ingredient in satellite_recipe.ingredients: @@ -383,15 +391,15 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ "uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing") progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb") progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron") -progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon", - "automated-rail-transportation", "rail-signals") +progressive_rows["progressive-fluid-handling"] = ("fluid-handling", "fluid-wagon") +progressive_rows["progressive-train-network"] = ("railway", "automated-rail-transportation") progressive_rows["progressive-engine"] = ("engine", "electric-engine") progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2") progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment") progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "energy-shield-mk2-equipment") progressive_rows["progressive-wall"] = ("stone-wall", "gate") progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer") -progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter") +progressive_rows["progressive-inserter"] = ("fast-inserter", "bulk-inserter") progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret") progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment", @@ -403,7 +411,7 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ source_target_mapping: Dict[str, str] = { "progressive-braking-force": "progressive-train-network", "progressive-inserter-capacity-bonus": "progressive-inserter", - "progressive-refined-flammables": "progressive-flamethrower" + "progressive-refined-flammables": "progressive-flamethrower", } for source, target in source_target_mapping.items(): @@ -417,12 +425,14 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ for root in sorted_rows: progressive = progressive_rows[root] - assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" + assert all(tech in tech_table for tech in progressive), \ + (f"Declared a progressive technology ({root}) without base technology. " + f"Missing: f{tuple(tech for tech in progressive if tech not in tech_table)}") factorio_tech_id += 1 - progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id, - progressive, + progressive_technology = Technology(root, factorio_tech_id, + tuple(progressive), has_modifier=any(technology_table[tech].has_modifier for tech in progressive), - unlocks=any(technology_table[tech].unlocks for tech in progressive)) + unlocks=any(technology_table[tech].unlocks for tech in progressive),) progressive_tech_table[root] = progressive_technology.factorio_id progressive_technology_table[root] = progressive_technology diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 1ea2f6e4c98c..8f8abeb292f1 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -2,19 +2,21 @@ import collections import logging -import settings import typing -from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification +import Utils +import settings +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld from worlds.LauncherComponents import Component, components, Type, launch_subprocess 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, option_groups) 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, \ + all_product_sources, required_technologies, get_rocket_requirements, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ fluids, stacking_items, valid_ingredients, progressive_rows @@ -60,6 +62,7 @@ class FactorioWeb(WebWorld): "setup/en", ["Berserker, Farrak Kilhn"] )] + option_groups = option_groups class FactorioItem(Item): @@ -74,6 +77,7 @@ class FactorioItem(Item): all_items["Cluster Grenade Trap"] = factorio_base_id - 5 all_items["Artillery Trap"] = factorio_base_id - 6 all_items["Atomic Rocket Trap"] = factorio_base_id - 7 +all_items["Atomic Cliff Remover Trap"] = factorio_base_id - 8 class Factorio(World): @@ -89,24 +93,29 @@ 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, 1) + if Utils.version_tuple < required_client_version: + raise Exception(f"Update Archipelago to use this world ({game}).") 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] - + removed_technologies: typing.Set[str] settings: typing.ClassVar[FactorioSettings] def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) + self.removed_technologies = useless_technologies.copy() self.advancement_technologies = set() self.custom_recipes = {} self.science_locations = [] @@ -116,35 +125,33 @@ 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.atomic_cliff_remover_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 +159,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 +170,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,43 +185,40 @@ 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") + traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket", + "Atomic Cliff Remover") 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 if self.skip_silo: - removed = useless_technologies | {"rocket-silo"} - else: - removed = useless_technologies + self.removed_technologies |= {"rocket-silo"} for tech_name in base_tech_table: - if tech_name not in removed: + if tech_name not in self.removed_technologies: progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name) want_progressive = want_progressives[progressive_item_name] item_name = progressive_item_name if want_progressive else tech_name @@ -232,58 +236,66 @@ 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: \ - (ingredient not in technology_table or state.has(ingredient, player)) and \ + (not technology_table[ingredient].unlocks or state.has(ingredient, player)) and \ all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients for technology in required_technologies[sub_ingredient]) and \ all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine]) + else: location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) for location in self.science_locations: - Rules.set_rule(location, lambda state, ingredients=location.ingredients: + Rules.set_rule(location, lambda state, ingredients=frozenset(location.ingredients): all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) prerequisites = shapes.get(location) if prerequisites: - Rules.add_rule(location, lambda state, locations= - prerequisites: all(state.can_reach(loc) for loc in locations)) + Rules.add_rule(location, lambda state, locations=frozenset(prerequisites): + all(state.can_reach(loc) for loc in locations)) silo_recipe = None - if self.multiworld.silo[self.player] == 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"))) + cargo_pad_recipe = None + if self.options.silo == Silo.option_spawn: + silo_recipe = self.get_recipe("rocket-silo") + cargo_pad_recipe = self.get_recipe("cargo-landing-pad") part_recipe = self.custom_recipes["rocket-part"] satellite_recipe = None - if self.multiworld.goal[self.player] == 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: - 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) - - world.completion_condition[player] = lambda state: state.has('Victory', player) + if self.options.goal == Goal.option_satellite: + satellite_recipe = self.get_recipe("satellite") + victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe, cargo_pad_recipe) + if self.options.silo == Silo.option_spawn: + victory_tech_names -= {"rocket-silo"} + else: + victory_tech_names |= {"rocket-silo"} + self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) + for technology in + victory_tech_names) + for tech_name in victory_tech_names: + if not self.multiworld.get_all_state(True).has(tech_name, player): + print(tech_name) + self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) + + def get_recipe(self, name: str) -> Recipe: + return self.custom_recipes[name] if name in self.custom_recipes \ + else next(iter(all_product_sources.get(name))) 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 +319,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) @@ -326,9 +336,11 @@ def get_category(category: str, liquids: int) -> str: def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2, ingredients_offset: int = 0) -> Recipe: + count: int = len(original.ingredients) + ingredients_offset + assert len(pool) >= count, f"Can't pick {count} many items from pool {pool}." new_ingredients = {} liquids_used = 0 - for _ in range(len(original.ingredients) + ingredients_offset): + for _ in range(count): new_ingredient = pool.pop() if new_ingredient in fluids: while liquids_used == allow_liquids and new_ingredient in fluids: @@ -348,7 +360,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 +404,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 +448,67 @@ 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 |= {"rocket-silo"} - if self.multiworld.goal[self.player].value == Goal.option_satellite: + needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} + if self.options.silo != Silo.option_spawn: + needed_recipes |= {"rocket-silo", "cargo-landing-pad"} + if self.options.goal.value == Goal.option_satellite: needed_recipes |= {"satellite"} for recipe in needed_recipes: @@ -545,7 +558,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/factorio/data/fluids.json b/worlds/factorio/data/fluids.json index 448ccf4e4921..6972690f5355 100644 --- a/worlds/factorio/data/fluids.json +++ b/worlds/factorio/data/fluids.json @@ -1 +1 @@ -["fluid-unknown","water","crude-oil","steam","heavy-oil","light-oil","petroleum-gas","sulfuric-acid","lubricant"] \ No newline at end of file +["water","steam","crude-oil","petroleum-gas","light-oil","heavy-oil","lubricant","sulfuric-acid","parameter-0","parameter-1","parameter-2","parameter-3","parameter-4","parameter-5","parameter-6","parameter-7","parameter-8","parameter-9","fluid-unknown"] \ No newline at end of file diff --git a/worlds/factorio/data/items.json b/worlds/factorio/data/items.json index fa34430f40c4..d9ec7befba90 100644 --- a/worlds/factorio/data/items.json +++ b/worlds/factorio/data/items.json @@ -1 +1 @@ -{"wooden-chest":50,"iron-chest":50,"steel-chest":50,"storage-tank":50,"transport-belt":100,"fast-transport-belt":100,"express-transport-belt":100,"underground-belt":50,"fast-underground-belt":50,"express-underground-belt":50,"splitter":50,"fast-splitter":50,"express-splitter":50,"loader":50,"fast-loader":50,"express-loader":50,"burner-inserter":50,"inserter":50,"long-handed-inserter":50,"fast-inserter":50,"filter-inserter":50,"stack-inserter":50,"stack-filter-inserter":50,"small-electric-pole":50,"medium-electric-pole":50,"big-electric-pole":50,"substation":50,"pipe":100,"pipe-to-ground":50,"pump":50,"rail":100,"train-stop":10,"rail-signal":50,"rail-chain-signal":50,"locomotive":5,"cargo-wagon":5,"fluid-wagon":5,"artillery-wagon":5,"car":1,"tank":1,"spidertron":1,"spidertron-remote":1,"logistic-robot":50,"construction-robot":50,"logistic-chest-active-provider":50,"logistic-chest-passive-provider":50,"logistic-chest-storage":50,"logistic-chest-buffer":50,"logistic-chest-requester":50,"roboport":10,"small-lamp":50,"red-wire":200,"green-wire":200,"arithmetic-combinator":50,"decider-combinator":50,"constant-combinator":50,"power-switch":50,"programmable-speaker":50,"stone-brick":100,"concrete":100,"hazard-concrete":100,"refined-concrete":100,"refined-hazard-concrete":100,"landfill":100,"cliff-explosives":20,"dummy-steel-axe":1,"repair-pack":100,"blueprint":1,"deconstruction-planner":1,"upgrade-planner":1,"blueprint-book":1,"copy-paste-tool":1,"cut-paste-tool":1,"boiler":50,"steam-engine":10,"solar-panel":50,"accumulator":50,"nuclear-reactor":10,"heat-pipe":50,"heat-exchanger":50,"steam-turbine":10,"burner-mining-drill":50,"electric-mining-drill":50,"offshore-pump":20,"pumpjack":20,"stone-furnace":50,"steel-furnace":50,"electric-furnace":50,"assembling-machine-1":50,"assembling-machine-2":50,"assembling-machine-3":50,"oil-refinery":10,"chemical-plant":10,"centrifuge":50,"lab":10,"beacon":10,"speed-module":50,"speed-module-2":50,"speed-module-3":50,"effectivity-module":50,"effectivity-module-2":50,"effectivity-module-3":50,"productivity-module":50,"productivity-module-2":50,"productivity-module-3":50,"rocket-silo":1,"satellite":1,"wood":100,"coal":50,"stone":50,"iron-ore":50,"copper-ore":50,"uranium-ore":50,"raw-fish":100,"iron-plate":100,"copper-plate":100,"solid-fuel":50,"steel-plate":100,"plastic-bar":100,"sulfur":50,"battery":200,"explosives":50,"crude-oil-barrel":10,"heavy-oil-barrel":10,"light-oil-barrel":10,"lubricant-barrel":10,"petroleum-gas-barrel":10,"sulfuric-acid-barrel":10,"water-barrel":10,"copper-cable":200,"iron-stick":100,"iron-gear-wheel":100,"empty-barrel":10,"electronic-circuit":200,"advanced-circuit":200,"processing-unit":100,"engine-unit":50,"electric-engine-unit":50,"flying-robot-frame":50,"rocket-control-unit":10,"low-density-structure":10,"rocket-fuel":10,"rocket-part":5,"nuclear-fuel":1,"uranium-235":100,"uranium-238":100,"uranium-fuel-cell":50,"used-up-uranium-fuel-cell":50,"automation-science-pack":200,"logistic-science-pack":200,"military-science-pack":200,"chemical-science-pack":200,"production-science-pack":200,"utility-science-pack":200,"space-science-pack":2000,"coin":100000,"pistol":5,"submachine-gun":5,"tank-machine-gun":1,"vehicle-machine-gun":1,"tank-flamethrower":1,"shotgun":5,"combat-shotgun":5,"rocket-launcher":5,"flamethrower":5,"land-mine":100,"artillery-wagon-cannon":1,"spidertron-rocket-launcher-1":1,"spidertron-rocket-launcher-2":1,"spidertron-rocket-launcher-3":1,"spidertron-rocket-launcher-4":1,"tank-cannon":1,"firearm-magazine":200,"piercing-rounds-magazine":200,"uranium-rounds-magazine":200,"shotgun-shell":200,"piercing-shotgun-shell":200,"cannon-shell":200,"explosive-cannon-shell":200,"uranium-cannon-shell":200,"explosive-uranium-cannon-shell":200,"artillery-shell":1,"rocket":200,"explosive-rocket":200,"atomic-bomb":10,"flamethrower-ammo":100,"grenade":100,"cluster-grenade":100,"poison-capsule":100,"slowdown-capsule":100,"defender-capsule":100,"distractor-capsule":100,"destroyer-capsule":100,"light-armor":1,"heavy-armor":1,"modular-armor":1,"power-armor":1,"power-armor-mk2":1,"solar-panel-equipment":20,"fusion-reactor-equipment":20,"battery-equipment":20,"battery-mk2-equipment":20,"belt-immunity-equipment":20,"exoskeleton-equipment":20,"personal-roboport-equipment":20,"personal-roboport-mk2-equipment":20,"night-vision-equipment":20,"energy-shield-equipment":20,"energy-shield-mk2-equipment":20,"personal-laser-defense-equipment":20,"discharge-defense-equipment":20,"discharge-defense-remote":1,"stone-wall":100,"gate":50,"gun-turret":50,"laser-turret":50,"flamethrower-turret":50,"artillery-turret":10,"artillery-targeting-remote":1,"radar":50,"player-port":50,"item-unknown":1,"electric-energy-interface":50,"linked-chest":10,"heat-interface":20,"linked-belt":10,"infinity-chest":10,"infinity-pipe":10,"selection-tool":1,"item-with-inventory":1,"item-with-label":1,"item-with-tags":1,"simple-entity-with-force":50,"simple-entity-with-owner":50,"burner-generator":10} \ No newline at end of file +{"wooden-chest":50,"iron-chest":50,"steel-chest":50,"storage-tank":50,"transport-belt":100,"fast-transport-belt":100,"express-transport-belt":100,"underground-belt":50,"fast-underground-belt":50,"express-underground-belt":50,"splitter":50,"fast-splitter":50,"express-splitter":50,"loader":50,"fast-loader":50,"express-loader":50,"burner-inserter":50,"inserter":50,"long-handed-inserter":50,"fast-inserter":50,"bulk-inserter":50,"small-electric-pole":50,"medium-electric-pole":50,"big-electric-pole":50,"substation":50,"pipe":100,"pipe-to-ground":50,"pump":50,"rail":100,"train-stop":10,"rail-signal":50,"rail-chain-signal":50,"locomotive":5,"cargo-wagon":5,"fluid-wagon":5,"artillery-wagon":5,"car":1,"tank":1,"spidertron":1,"logistic-robot":50,"construction-robot":50,"active-provider-chest":50,"passive-provider-chest":50,"storage-chest":50,"buffer-chest":50,"requester-chest":50,"roboport":10,"small-lamp":50,"arithmetic-combinator":50,"decider-combinator":50,"selector-combinator":50,"constant-combinator":50,"power-switch":10,"programmable-speaker":10,"display-panel":10,"stone-brick":100,"concrete":100,"hazard-concrete":100,"refined-concrete":100,"refined-hazard-concrete":100,"landfill":100,"cliff-explosives":20,"repair-pack":100,"blueprint":1,"deconstruction-planner":1,"upgrade-planner":1,"blueprint-book":1,"copy-paste-tool":1,"cut-paste-tool":1,"boiler":50,"steam-engine":10,"solar-panel":50,"accumulator":50,"nuclear-reactor":10,"heat-pipe":50,"heat-exchanger":50,"steam-turbine":10,"burner-mining-drill":50,"electric-mining-drill":50,"offshore-pump":20,"pumpjack":20,"stone-furnace":50,"steel-furnace":50,"electric-furnace":50,"assembling-machine-1":50,"assembling-machine-2":50,"assembling-machine-3":50,"oil-refinery":10,"chemical-plant":10,"centrifuge":50,"lab":10,"beacon":20,"speed-module":50,"speed-module-2":50,"speed-module-3":50,"efficiency-module":50,"efficiency-module-2":50,"efficiency-module-3":50,"productivity-module":50,"productivity-module-2":50,"productivity-module-3":50,"empty-module-slot":1,"rocket-silo":1,"cargo-landing-pad":1,"satellite":1,"wood":100,"coal":50,"stone":50,"iron-ore":50,"copper-ore":50,"uranium-ore":50,"raw-fish":100,"iron-plate":100,"copper-plate":100,"steel-plate":100,"solid-fuel":50,"plastic-bar":100,"sulfur":50,"battery":200,"explosives":50,"water-barrel":10,"crude-oil-barrel":10,"petroleum-gas-barrel":10,"light-oil-barrel":10,"heavy-oil-barrel":10,"lubricant-barrel":10,"sulfuric-acid-barrel":10,"iron-gear-wheel":100,"iron-stick":100,"copper-cable":200,"barrel":10,"electronic-circuit":200,"advanced-circuit":200,"processing-unit":100,"engine-unit":50,"electric-engine-unit":50,"flying-robot-frame":50,"low-density-structure":50,"rocket-fuel":20,"rocket-part":5,"uranium-235":100,"uranium-238":100,"uranium-fuel-cell":50,"depleted-uranium-fuel-cell":50,"nuclear-fuel":1,"automation-science-pack":200,"logistic-science-pack":200,"military-science-pack":200,"chemical-science-pack":200,"production-science-pack":200,"utility-science-pack":200,"space-science-pack":2000,"coin":100000,"science":1,"pistol":5,"submachine-gun":5,"tank-machine-gun":1,"vehicle-machine-gun":1,"tank-flamethrower":1,"shotgun":5,"combat-shotgun":5,"rocket-launcher":5,"flamethrower":5,"artillery-wagon-cannon":1,"spidertron-rocket-launcher-1":1,"spidertron-rocket-launcher-2":1,"spidertron-rocket-launcher-3":1,"spidertron-rocket-launcher-4":1,"tank-cannon":1,"firearm-magazine":100,"piercing-rounds-magazine":100,"uranium-rounds-magazine":100,"shotgun-shell":100,"piercing-shotgun-shell":100,"cannon-shell":100,"explosive-cannon-shell":100,"uranium-cannon-shell":100,"explosive-uranium-cannon-shell":100,"artillery-shell":1,"rocket":100,"explosive-rocket":100,"atomic-bomb":10,"flamethrower-ammo":100,"grenade":100,"cluster-grenade":100,"poison-capsule":100,"slowdown-capsule":100,"defender-capsule":100,"distractor-capsule":100,"destroyer-capsule":100,"light-armor":1,"heavy-armor":1,"modular-armor":1,"power-armor":1,"power-armor-mk2":1,"solar-panel-equipment":20,"fission-reactor-equipment":20,"battery-equipment":20,"battery-mk2-equipment":20,"belt-immunity-equipment":20,"exoskeleton-equipment":20,"personal-roboport-equipment":20,"personal-roboport-mk2-equipment":20,"night-vision-equipment":20,"energy-shield-equipment":20,"energy-shield-mk2-equipment":20,"personal-laser-defense-equipment":20,"discharge-defense-equipment":20,"stone-wall":100,"gate":50,"radar":50,"land-mine":100,"gun-turret":50,"laser-turret":50,"flamethrower-turret":50,"artillery-turret":10,"parameter-0":1,"parameter-1":1,"parameter-2":1,"parameter-3":1,"parameter-4":1,"parameter-5":1,"parameter-6":1,"parameter-7":1,"parameter-8":1,"parameter-9":1,"copper-wire":1,"green-wire":1,"red-wire":1,"spidertron-remote":1,"discharge-defense-remote":1,"artillery-targeting-remote":1,"item-unknown":1,"electric-energy-interface":50,"linked-chest":10,"heat-interface":20,"lane-splitter":50,"linked-belt":10,"infinity-chest":10,"infinity-pipe":10,"selection-tool":1,"simple-entity-with-force":50,"simple-entity-with-owner":50,"burner-generator":10} \ No newline at end of file diff --git a/worlds/factorio/data/machines.json b/worlds/factorio/data/machines.json index 15a79580d060..c8629ab8bef0 100644 --- a/worlds/factorio/data/machines.json +++ b/worlds/factorio/data/machines.json @@ -1 +1 @@ -{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}} \ No newline at end of file +{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true,"parameters":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true,"parameters":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true,"parameters":true},"oil-refinery":{"oil-processing":true,"parameters":true},"chemical-plant":{"chemistry":true,"parameters":true},"centrifuge":{"centrifuging":true,"parameters":true},"rocket-silo":{"rocket-building":true,"parameters":true},"character":{"crafting":true}} \ No newline at end of file diff --git a/worlds/factorio/data/mod/lib.lua b/worlds/factorio/data/mod/lib.lua index 2b18f119a427..517a54e3d642 100644 --- a/worlds/factorio/data/mod/lib.lua +++ b/worlds/factorio/data/mod/lib.lua @@ -1,9 +1,9 @@ function get_any_stack_size(name) - local item = game.item_prototypes[name] + local item = prototypes.item[name] if item ~= nil then return item.stack_size end - item = game.equipment_prototypes[name] + item = prototypes.equipment[name] if item ~= nil then return item.stack_size end @@ -24,16 +24,27 @@ function split(s, sep) end function random_offset_position(position, offset) - return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-1024, 1024)} + return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-offset, offset)} end function fire_entity_at_players(entity_name, speed) + local entities = {} for _, player in ipairs(game.forces["player"].players) do - current_character = player.character - if current_character ~= nil then - current_character.surface.create_entity{name=entity_name, - position=random_offset_position(current_character.position, 128), - target=current_character, speed=speed} + if player.character ~= nil then + table.insert(entities, player.character) end end -end \ No newline at end of file + return fire_entity_at_entities(entity_name, entities, speed) +end + +function fire_entity_at_entities(entity_name, entities, speed) + for _, current_entity in ipairs(entities) do + local target = current_entity + if target.health == nil then + target = target.position + end + current_entity.surface.create_entity{name=entity_name, + position=random_offset_position(current_entity.position, 128), + target=target, speed=speed} + end +end diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 8ce0b45a5f67..e486c7433095 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -105,8 +105,8 @@ function on_player_changed_position(event) end local target_direction = exit_table[outbound_direction] - local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, - (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} + local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, + (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} target_position = character.surface.find_non_colliding_position(character.prototype.name, target_position, 32, 0.5) if target_position ~= nil then @@ -134,40 +134,96 @@ end script.on_event(defines.events.on_player_changed_position, on_player_changed_position) {% endif %} - +function count_energy_bridges() + local count = 0 + for i, bridge in pairs(storage.energy_link_bridges) do + if validate_energy_link_bridge(i, bridge) then + count = count + 1 + (bridge.quality.level * 0.3) + end + end + return count +end +function get_energy_increment(bridge) + return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level) +end function on_check_energy_link(event) --- assuming 1 MJ increment and 5MJ battery: --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing if event.tick % 60 == 30 then - local surface = game.get_surface(1) local force = "player" - local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force}) - local bridgecount = table_size(bridges) - global.forcedata[force].energy_bridges = bridgecount - if global.forcedata[force].energy == nil then - global.forcedata[force].energy = 0 + local bridges = storage.energy_link_bridges + local bridgecount = count_energy_bridges() + storage.forcedata[force].energy_bridges = bridgecount + if storage.forcedata[force].energy == nil then + storage.forcedata[force].energy = 0 end - if global.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then - for i, bridge in ipairs(bridges) do - if bridge.energy > ENERGY_INCREMENT*3 then - global.forcedata[force].energy = global.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY) - bridge.energy = bridge.energy - ENERGY_INCREMENT + if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if bridge.energy > energy_increment*3 then + storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY) + bridge.energy = bridge.energy - energy_increment + end end end end - for i, bridge in ipairs(bridges) do - if global.forcedata[force].energy < ENERGY_INCREMENT then - break - end - if bridge.energy < ENERGY_INCREMENT*2 and global.forcedata[force].energy > ENERGY_INCREMENT then - global.forcedata[force].energy = global.forcedata[force].energy - ENERGY_INCREMENT - bridge.energy = bridge.energy + ENERGY_INCREMENT + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then + break + end + if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then + storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment + bridge.energy = bridge.energy + energy_increment + end end end end end +function string_starts_with(str, start) + return str:sub(1, #start) == start +end +function validate_energy_link_bridge(unit_number, entity) + if not entity then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + if not entity.valid then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + return true +end +function on_energy_bridge_constructed(entity) + if entity and entity.valid then + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + storage.energy_link_bridges[entity.unit_number] = entity + end + end +end +function on_energy_bridge_removed(entity) + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + if storage.energy_link_bridges[entity.unit_number] == nil then return end + storage.energy_link_bridges[entity.unit_number] = nil + end +end if (ENERGY_INCREMENT) then script.on_event(defines.events.on_tick, on_check_energy_link) + + script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end) + + script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end) + + script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) end {% if not imported_blueprints -%} @@ -186,23 +242,41 @@ function check_spawn_silo(force) local surface = game.get_surface(1) local spawn_position = force.get_spawn_position(surface) spawn_entity(surface, force, "rocket-silo", spawn_position.x, spawn_position.y, 80, true, true) + spawn_entity(surface, force, "cargo-landing-pad", spawn_position.x, spawn_position.y, 80, true, true) end end function check_despawn_silo(force) - if not force.players or #force.players < 1 and force.get_entity_count("rocket-silo") > 0 then - local surface = game.get_surface(1) - local spawn_position = force.get_spawn_position(surface) - local x1 = spawn_position.x - 41 - local x2 = spawn_position.x + 41 - local y1 = spawn_position.y - 41 - local y2 = spawn_position.y + 41 - local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, - name = "rocket-silo", - force = force} - for i,silo in ipairs(silos) do - silo.destructible = true - silo.destroy() + if not force.players or #force.players < 1 then + if force.get_entity_count("rocket-silo") > 0 then + local surface = game.get_surface(1) + local spawn_position = force.get_spawn_position(surface) + local x1 = spawn_position.x - 41 + local x2 = spawn_position.x + 41 + local y1 = spawn_position.y - 41 + local y2 = spawn_position.y + 41 + local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, + name = "rocket-silo", + force = force} + for i, silo in ipairs(silos) do + silo.destructible = true + silo.destroy() + end + end + if force.get_entity_count("cargo-landing-pad") > 0 then + local surface = game.get_surface(1) + local spawn_position = force.get_spawn_position(surface) + local x1 = spawn_position.x - 41 + local x2 = spawn_position.x + 41 + local y1 = spawn_position.y - 41 + local y2 = spawn_position.y + 41 + local pads = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, + name = "cargo-landing-pad", + force = force} + for i, pad in ipairs(pads) do + pad.destructible = true + pad.destroy() + end end end end @@ -214,19 +288,18 @@ function on_force_created(event) if type(event.force) == "string" then -- should be of type LuaForce force = game.forces[force] end - force.research_queue_enabled = true local data = {} data['earned_samples'] = {{ dict_to_lua(starting_items) }} data["victory"] = 0 data["death_link_tick"] = 0 data["energy"] = 0 data["energy_bridges"] = 0 - global.forcedata[event.force] = data + storage.forcedata[event.force] = data {%- if silo == 2 %} check_spawn_silo(force) {%- endif %} -{%- for tech_name in useless_technologies %} - force.technologies.{{ tech_name }}.researched = true +{%- for tech_name in removed_technologies %} + force.technologies["{{ tech_name }}"].researched = true {%- endfor %} end script.on_event(defines.events.on_force_created, on_force_created) @@ -236,7 +309,7 @@ function on_force_destroyed(event) {%- if silo == 2 %} check_despawn_silo(event.force) {%- endif %} - global.forcedata[event.force.name] = nil + storage.forcedata[event.force.name] = nil end function on_runtime_mod_setting_changed(event) @@ -267,8 +340,8 @@ function on_player_created(event) -- FIXME: This (probably) fires before any other mod has a chance to change the player's force -- For now, they will (probably) always be on the 'player' force when this event fires. local data = {} - data['pending_samples'] = table.deepcopy(global.forcedata[player.force.name]['earned_samples']) - global.playerdata[player.index] = data + data['pending_samples'] = table.deepcopy(storage.forcedata[player.force.name]['earned_samples']) + storage.playerdata[player.index] = data update_player(player.index) -- Attempt to send pending free samples, if relevant. {%- if silo == 2 %} check_spawn_silo(game.players[event.player_index].force) @@ -287,14 +360,19 @@ end script.on_event(defines.events.on_player_changed_force, on_player_changed_force) function on_player_removed(event) - global.playerdata[event.player_index] = nil + storage.playerdata[event.player_index] = nil end script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) - if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then - if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then - global.forcedata[event.rocket.force.name]['victory'] = 1 + if event.rocket and event.rocket.valid and storage.forcedata[event.rocket.force.name]['victory'] == 0 then + satellite_count = 0 + cargo_pod = event.rocket.cargo_pod + if cargo_pod then + satellite_count = cargo_pod.get_item_count("satellite") + end + if satellite_count > 0 or GOAL == 0 then + storage.forcedata[event.rocket.force.name]['victory'] = 1 dumpInfo(event.rocket.force) game.set_game_state { @@ -318,7 +396,7 @@ function update_player(index) if not character or not character.valid then return end - local data = global.playerdata[index] + local data = storage.playerdata[index] local samples = data['pending_samples'] local sent --player.print(serpent.block(data['pending_samples'])) @@ -327,14 +405,17 @@ function update_player(index) for name, count in pairs(samples) do stack.name = name stack.count = count - if game.item_prototypes[name] then + if script.active_mods["quality"] then + stack.quality = "{{ free_sample_quality_name }}" + end + if prototypes.item[name] then if character.can_insert(stack) then sent = character.insert(stack) else sent = 0 end if sent > 0 then - player.print("Received " .. sent .. "x [item=" .. name .. "]") + player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]") data.suppress_full_inventory_message = false end if sent ~= count then -- Couldn't full send. @@ -372,19 +453,20 @@ function add_samples(force, name, count) end t[name] = (t[name] or 0) + count end - -- Add to global table of earned samples for future new players - add_to_table(global.forcedata[force.name]['earned_samples']) + -- Add to storage table of earned samples for future new players + add_to_table(storage.forcedata[force.name]['earned_samples']) -- Add to existing players for _, player in pairs(force.players) do - add_to_table(global.playerdata[player.index]['pending_samples']) + add_to_table(storage.playerdata[player.index]['pending_samples']) update_player(player.index) end end script.on_init(function() {% if not imported_blueprints %}set_permissions(){% endif %} - global.forcedata = {} - global.playerdata = {} + storage.forcedata = {} + storage.playerdata = {} + storage.energy_link_bridges = {} -- Fire dummy events for all currently existing forces. local e = {} for name, _ in pairs(game.forces) do @@ -420,12 +502,12 @@ script.on_event(defines.events.on_research_finished, function(event) if FREE_SAMPLES == 0 then return -- Nothing else to do end - if not technology.effects then + if not technology.prototype.effects then return -- No technology effects, so nothing to do. end - for _, effect in pairs(technology.effects) do + for _, effect in pairs(technology.prototype.effects) do if effect.type == "unlock-recipe" then - local recipe = game.recipe_prototypes[effect.recipe] + local recipe = prototypes.recipe[effect.recipe] for _, result in pairs(recipe.products) do if result.type == "item" and result.amount then local name = result.name @@ -477,7 +559,7 @@ function kill_players(force) end function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) - local prototype = game.entity_prototypes[name] + local prototype = prototypes.entity[name] local args = { -- For can_place_entity and place_entity name = prototype.name, position = {x = x, y = y}, @@ -537,7 +619,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) } local entities = surface.find_entities_filtered { area = collision_area, - collision_mask = prototype.collision_mask + collision_mask = prototype.collision_mask.layers } local can_place = true for _, entity in pairs(entities) do @@ -560,6 +642,9 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) end args.build_check_type = defines.build_check_type.script args.create_build_effect_smoke = false + if script.active_mods["quality"] then + args.quality = "{{ free_sample_quality_name }}" + end new_entity = surface.create_entity(args) if new_entity then new_entity.destructible = false @@ -585,7 +670,7 @@ script.on_event(defines.events.on_entity_died, function(event) end local force = event.entity.force - global.forcedata[force.name].death_link_tick = game.tick + storage.forcedata[force.name].death_link_tick = game.tick dumpInfo(force) kill_players(force) end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}}) @@ -600,7 +685,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress force = game.players[call.player_index].force end local research_done = {} - local forcedata = chain_lookup(global, "forcedata", force.name) + local forcedata = chain_lookup(storage, "forcedata", force.name) local data_collection = { ["research_done"] = research_done, ["victory"] = chain_lookup(forcedata, "victory"), @@ -616,7 +701,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress research_done[tech_name] = tech.researched end end - rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection})) + rcon.print(helpers.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection})) end) commands.add_command("ap-print", "Used by the Archipelago client to print messages", function (call) @@ -652,19 +737,33 @@ end, ["Atomic Rocket Trap"] = function () fire_entity_at_players("atomic-rocket", 0.1) end, +["Atomic Cliff Remover Trap"] = function () + local cliffs = game.surfaces["nauvis"].find_entities_filtered{type = "cliff"} + + if #cliffs > 0 then + fire_entity_at_entities("atomic-rocket", {cliffs[math.random(#cliffs)]}, 0.1) + end +end, } commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) - if global.index_sync == nil then - global.index_sync = {} + if storage.index_sync == nil then + storage.index_sync = {} end local tech local force = game.forces["player"] + if call.parameter == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + end chunks = split(call.parameter, "\t") local item_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - if index == -1 then -- for coop sync and restoring from an older savegame + if index == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) @@ -673,8 +772,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end return elseif progressive_technologies[item_name] ~= nil then - if global.index_sync[index] ~= item_name then -- not yet received prog item - global.index_sync[index] = item_name + if storage.index_sync[index] ~= item_name then -- not yet received prog item + storage.index_sync[index] = item_name local tech_stack = progressive_technologies[item_name] for _, item_name in ipairs(tech_stack) do tech = force.technologies[item_name] @@ -689,7 +788,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi elseif force.technologies[item_name] ~= nil then tech = force.technologies[item_name] if tech ~= nil then - global.index_sync[index] = tech + storage.index_sync[index] = tech if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) game.play_sound({path="utility/research_completed"}) @@ -697,8 +796,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end end elseif TRAP_TABLE[item_name] ~= nil then - if global.index_sync[index] ~= item_name then -- not yet received trap - global.index_sync[index] = item_name + if storage.index_sync[index] ~= item_name then -- not yet received trap + storage.index_sync[index] = item_name game.print({"", "Received ", item_name, " from ", source}) TRAP_TABLE[item_name]() end @@ -709,7 +808,7 @@ end) commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call) - rcon.print(game.table_to_json({ + rcon.print(helpers.table_to_json({ ["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["death_link"] = DEATH_LINK, @@ -719,7 +818,7 @@ end) {% if allow_cheats -%} -commands.add_command("ap-spawn-silo", "Attempts to spawn a silo around 0,0", function(call) +commands.add_command("ap-spawn-silo", "Attempts to spawn a silo and cargo landing pad around 0,0", function(call) spawn_entity(game.player.surface, game.player.force, "rocket-silo", 0, 0, 80, true, true) end) {% endif -%} @@ -735,7 +834,7 @@ end) commands.add_command("ap-energylink", "Used by the Archipelago client to manage Energy Link", function(call) local change = tonumber(call.parameter or "0") local force = "player" - global.forcedata[force].energy = global.forcedata[force].energy + change + storage.forcedata[force].energy = storage.forcedata[force].energy + change end) commands.add_command("energy-link", "Print the status of the Archipelago energy link.", function(call) diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 3021fd5dadca..dc068c4f62aa 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -6,43 +6,46 @@ data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, base_area = 10, base_level = -1, pipe_connections = { - { type = "input", position = { 0, 5 } }, - { type = "input", position = { 0, -5 } }, - { type = "input", position = { 5, 0 } }, - { type = "input", position = { -5, 0 } } + { flow_direction = "input", direction = defines.direction.south, position = { 0, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { 0, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, 0 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, 0 } } } }, { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, base_area = 10, base_level = -1, pipe_connections = { - { type = "input", position = { -3, 5 } }, - { type = "input", position = { -3, -5 } }, - { type = "input", position = { 5, -3 } }, - { type = "input", position = { -5, -3 } } + { flow_direction = "input", direction = defines.direction.south, position = { -3, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { -3, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, -3 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, -3 } } } }, { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, base_area = 10, base_level = -1, pipe_connections = { - { type = "input", position = { 3, 5 } }, - { type = "input", position = { 3, -5 } }, - { type = "input", position = { 5, 3 } }, - { type = "input", position = { -5, 3 } } + { flow_direction = "input", direction = defines.direction.south, position = { 3, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { 3, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, 3 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, 3 } } } - }, - off_when_no_fluid_recipe = true + } } +data.raw["rocket-silo"]["rocket-silo"].fluid_boxes_off_when_no_fluid_recipe = true {%- for recipe_name, recipe in custom_recipes.items() %} data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}" diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index 82053453eadf..43151ff00840 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -18,12 +18,9 @@ energy_bridge.energy_source.buffer_capacity = "50MJ" energy_bridge.energy_source.input_flow_limit = "10MW" energy_bridge.energy_source.output_flow_limit = "10MW" tint_icon(energy_bridge, energy_bridge_tint()) -energy_bridge.picture.layers[1].tint = energy_bridge_tint() -energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint() -energy_bridge.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() -energy_bridge.charge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() -energy_bridge.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() -energy_bridge.discharge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() +energy_bridge.chargable_graphics.picture.layers[1].tint = energy_bridge_tint() +energy_bridge.chargable_graphics.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() +energy_bridge.chargable_graphics.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"]) @@ -35,9 +32,9 @@ data.raw["item"]["ap-energy-bridge"] = energy_bridge_item local energy_bridge_recipe = table.deepcopy(data.raw["recipe"]["accumulator"]) energy_bridge_recipe.name = "ap-energy-bridge" -energy_bridge_recipe.result = energy_bridge_item.name +energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} } energy_bridge_recipe.energy_required = 1 -energy_bridge_recipe.enabled = {{ energy_link }} +energy_bridge_recipe.enabled = {% if energy_link %}true{% else %}false{% endif %} energy_bridge_recipe.localised_name = "Archipelago EnergyLink Bridge" data.raw["recipe"]["ap-energy-bridge"] = energy_bridge_recipe diff --git a/worlds/factorio/data/mod_template/macros.lua b/worlds/factorio/data/mod_template/macros.lua index 1b271031a393..f1530359c823 100644 --- a/worlds/factorio/data/mod_template/macros.lua +++ b/worlds/factorio/data/mod_template/macros.lua @@ -26,4 +26,4 @@ {type = {% if key in liquids %}"fluid"{% else %}"item"{% endif %}, name = "{{ key }}", amount = {{ value | safe }}}{% if not loop.last %},{% endif %} {% endfor -%} } -{%- endmacro %} \ No newline at end of file +{%- endmacro %} diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua index 73e131a60e7c..41d30e58d552 100644 --- a/worlds/factorio/data/mod_template/settings.lua +++ b/worlds/factorio/data/mod_template/settings.lua @@ -27,4 +27,4 @@ data:extend({ default_value = false {% endif %} } -}) \ No newline at end of file +}) diff --git a/worlds/factorio/data/recipes.json b/worlds/factorio/data/recipes.json index 4c4ab81526af..b0633b493d79 100644 --- a/worlds/factorio/data/recipes.json +++ b/worlds/factorio/data/recipes.json @@ -1 +1 @@ -{"accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting","energy":10},"advanced-circuit":{"ingredients":{"plastic-bar":2,"copper-cable":4,"electronic-circuit":2},"products":{"advanced-circuit":1},"category":"crafting","energy":6},"arithmetic-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"arithmetic-combinator":1},"category":"crafting","energy":0.5},"artillery-shell":{"ingredients":{"explosives":8,"explosive-cannon-shell":4,"radar":1},"products":{"artillery-shell":1},"category":"crafting","energy":15},"artillery-targeting-remote":{"ingredients":{"processing-unit":1,"radar":1},"products":{"artillery-targeting-remote":1},"category":"crafting","energy":0.5},"artillery-turret":{"ingredients":{"steel-plate":60,"iron-gear-wheel":40,"advanced-circuit":20,"concrete":60},"products":{"artillery-turret":1},"category":"crafting","energy":40},"artillery-wagon":{"ingredients":{"steel-plate":40,"iron-gear-wheel":10,"advanced-circuit":20,"engine-unit":64,"pipe":16},"products":{"artillery-wagon":1},"category":"crafting","energy":4},"assembling-machine-1":{"ingredients":{"iron-plate":9,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"assembling-machine-1":1},"category":"crafting","energy":0.5},"assembling-machine-2":{"ingredients":{"steel-plate":2,"iron-gear-wheel":5,"electronic-circuit":3,"assembling-machine-1":1},"products":{"assembling-machine-2":1},"category":"crafting","energy":0.5},"assembling-machine-3":{"ingredients":{"assembling-machine-2":2,"speed-module":4},"products":{"assembling-machine-3":1},"category":"crafting","energy":0.5},"atomic-bomb":{"ingredients":{"explosives":10,"rocket-control-unit":10,"uranium-235":30},"products":{"atomic-bomb":1},"category":"crafting","energy":50},"automation-science-pack":{"ingredients":{"copper-plate":1,"iron-gear-wheel":1},"products":{"automation-science-pack":1},"category":"crafting","energy":5},"battery":{"ingredients":{"iron-plate":1,"copper-plate":1,"sulfuric-acid":20},"products":{"battery":1},"category":"chemistry","energy":4},"battery-equipment":{"ingredients":{"steel-plate":10,"battery":5},"products":{"battery-equipment":1},"category":"crafting","energy":10},"battery-mk2-equipment":{"ingredients":{"processing-unit":15,"low-density-structure":5,"battery-equipment":10},"products":{"battery-mk2-equipment":1},"category":"crafting","energy":10},"beacon":{"ingredients":{"steel-plate":10,"copper-cable":10,"electronic-circuit":20,"advanced-circuit":20},"products":{"beacon":1},"category":"crafting","energy":15},"belt-immunity-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"belt-immunity-equipment":1},"category":"crafting","energy":10},"big-electric-pole":{"ingredients":{"copper-plate":5,"steel-plate":5,"iron-stick":8},"products":{"big-electric-pole":1},"category":"crafting","energy":0.5},"boiler":{"ingredients":{"pipe":4,"stone-furnace":1},"products":{"boiler":1},"category":"crafting","energy":0.5},"burner-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"burner-inserter":1},"category":"crafting","energy":0.5},"burner-mining-drill":{"ingredients":{"iron-plate":3,"iron-gear-wheel":3,"stone-furnace":1},"products":{"burner-mining-drill":1},"category":"crafting","energy":2},"cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":1},"products":{"cannon-shell":1},"category":"crafting","energy":8},"car":{"ingredients":{"iron-plate":20,"steel-plate":5,"engine-unit":8},"products":{"car":1},"category":"crafting","energy":2},"cargo-wagon":{"ingredients":{"iron-plate":20,"steel-plate":20,"iron-gear-wheel":10},"products":{"cargo-wagon":1},"category":"crafting","energy":1},"centrifuge":{"ingredients":{"steel-plate":50,"iron-gear-wheel":100,"advanced-circuit":100,"concrete":100},"products":{"centrifuge":1},"category":"crafting","energy":4},"chemical-plant":{"ingredients":{"steel-plate":5,"iron-gear-wheel":5,"electronic-circuit":5,"pipe":5},"products":{"chemical-plant":1},"category":"crafting","energy":5},"chemical-science-pack":{"ingredients":{"sulfur":1,"advanced-circuit":3,"engine-unit":2},"products":{"chemical-science-pack":2},"category":"crafting","energy":24},"cliff-explosives":{"ingredients":{"explosives":10,"empty-barrel":1,"grenade":1},"products":{"cliff-explosives":1},"category":"crafting","energy":8},"cluster-grenade":{"ingredients":{"steel-plate":5,"explosives":5,"grenade":7},"products":{"cluster-grenade":1},"category":"crafting","energy":8},"combat-shotgun":{"ingredients":{"wood":10,"copper-plate":10,"steel-plate":15,"iron-gear-wheel":5},"products":{"combat-shotgun":1},"category":"crafting","energy":10},"concrete":{"ingredients":{"iron-ore":1,"stone-brick":5,"water":100},"products":{"concrete":10},"category":"crafting-with-fluid","energy":10},"constant-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":2},"products":{"constant-combinator":1},"category":"crafting","energy":0.5},"construction-robot":{"ingredients":{"electronic-circuit":2,"flying-robot-frame":1},"products":{"construction-robot":1},"category":"crafting","energy":0.5},"copper-cable":{"ingredients":{"copper-plate":1},"products":{"copper-cable":2},"category":"crafting","energy":0.5},"copper-plate":{"ingredients":{"copper-ore":1},"products":{"copper-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"decider-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"decider-combinator":1},"category":"crafting","energy":0.5},"defender-capsule":{"ingredients":{"iron-gear-wheel":3,"electronic-circuit":3,"piercing-rounds-magazine":3},"products":{"defender-capsule":1},"category":"crafting","energy":8},"destroyer-capsule":{"ingredients":{"speed-module":1,"distractor-capsule":4},"products":{"destroyer-capsule":1},"category":"crafting","energy":15},"discharge-defense-equipment":{"ingredients":{"steel-plate":20,"processing-unit":5,"laser-turret":10},"products":{"discharge-defense-equipment":1},"category":"crafting","energy":10},"discharge-defense-remote":{"ingredients":{"electronic-circuit":1},"products":{"discharge-defense-remote":1},"category":"crafting","energy":0.5},"distractor-capsule":{"ingredients":{"advanced-circuit":3,"defender-capsule":4},"products":{"distractor-capsule":1},"category":"crafting","energy":15},"effectivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"effectivity-module":1},"category":"crafting","energy":15},"effectivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"effectivity-module":4},"products":{"effectivity-module-2":1},"category":"crafting","energy":30},"effectivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"effectivity-module-2":5},"products":{"effectivity-module-3":1},"category":"crafting","energy":60},"electric-engine-unit":{"ingredients":{"electronic-circuit":2,"engine-unit":1,"lubricant":15},"products":{"electric-engine-unit":1},"category":"crafting-with-fluid","energy":10},"electric-furnace":{"ingredients":{"steel-plate":10,"advanced-circuit":5,"stone-brick":10},"products":{"electric-furnace":1},"category":"crafting","energy":5},"electric-mining-drill":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"electric-mining-drill":1},"category":"crafting","energy":2},"electronic-circuit":{"ingredients":{"iron-plate":1,"copper-cable":3},"products":{"electronic-circuit":1},"category":"crafting","energy":0.5},"empty-barrel":{"ingredients":{"steel-plate":1},"products":{"empty-barrel":1},"category":"crafting","energy":1},"energy-shield-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"energy-shield-equipment":1},"category":"crafting","energy":10},"energy-shield-mk2-equipment":{"ingredients":{"processing-unit":5,"low-density-structure":5,"energy-shield-equipment":10},"products":{"energy-shield-mk2-equipment":1},"category":"crafting","energy":10},"engine-unit":{"ingredients":{"steel-plate":1,"iron-gear-wheel":1,"pipe":2},"products":{"engine-unit":1},"category":"advanced-crafting","energy":10},"exoskeleton-equipment":{"ingredients":{"steel-plate":20,"processing-unit":10,"electric-engine-unit":30},"products":{"exoskeleton-equipment":1},"category":"crafting","energy":10},"explosive-cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":2},"products":{"explosive-cannon-shell":1},"category":"crafting","energy":8},"explosive-rocket":{"ingredients":{"explosives":2,"rocket":1},"products":{"explosive-rocket":1},"category":"crafting","energy":8},"explosive-uranium-cannon-shell":{"ingredients":{"uranium-238":1,"explosive-cannon-shell":1},"products":{"explosive-uranium-cannon-shell":1},"category":"crafting","energy":12},"explosives":{"ingredients":{"coal":1,"sulfur":1,"water":10},"products":{"explosives":2},"category":"chemistry","energy":4},"express-splitter":{"ingredients":{"iron-gear-wheel":10,"advanced-circuit":10,"fast-splitter":1,"lubricant":80},"products":{"express-splitter":1},"category":"crafting-with-fluid","energy":2},"express-transport-belt":{"ingredients":{"iron-gear-wheel":10,"fast-transport-belt":1,"lubricant":20},"products":{"express-transport-belt":1},"category":"crafting-with-fluid","energy":0.5},"express-underground-belt":{"ingredients":{"iron-gear-wheel":80,"fast-underground-belt":2,"lubricant":40},"products":{"express-underground-belt":2},"category":"crafting-with-fluid","energy":2},"fast-inserter":{"ingredients":{"iron-plate":2,"electronic-circuit":2,"inserter":1},"products":{"fast-inserter":1},"category":"crafting","energy":0.5},"fast-splitter":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"splitter":1},"products":{"fast-splitter":1},"category":"crafting","energy":2},"fast-transport-belt":{"ingredients":{"iron-gear-wheel":5,"transport-belt":1},"products":{"fast-transport-belt":1},"category":"crafting","energy":0.5},"fast-underground-belt":{"ingredients":{"iron-gear-wheel":40,"underground-belt":2},"products":{"fast-underground-belt":2},"category":"crafting","energy":2},"filter-inserter":{"ingredients":{"electronic-circuit":4,"fast-inserter":1},"products":{"filter-inserter":1},"category":"crafting","energy":0.5},"firearm-magazine":{"ingredients":{"iron-plate":4},"products":{"firearm-magazine":1},"category":"crafting","energy":1},"flamethrower":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10},"products":{"flamethrower":1},"category":"crafting","energy":10},"flamethrower-ammo":{"ingredients":{"steel-plate":5,"crude-oil":100},"products":{"flamethrower-ammo":1},"category":"chemistry","energy":6},"flamethrower-turret":{"ingredients":{"steel-plate":30,"iron-gear-wheel":15,"engine-unit":5,"pipe":10},"products":{"flamethrower-turret":1},"category":"crafting","energy":20},"fluid-wagon":{"ingredients":{"steel-plate":16,"iron-gear-wheel":10,"storage-tank":1,"pipe":8},"products":{"fluid-wagon":1},"category":"crafting","energy":1.5},"flying-robot-frame":{"ingredients":{"steel-plate":1,"battery":2,"electronic-circuit":3,"electric-engine-unit":1},"products":{"flying-robot-frame":1},"category":"crafting","energy":20},"fusion-reactor-equipment":{"ingredients":{"processing-unit":200,"low-density-structure":50},"products":{"fusion-reactor-equipment":1},"category":"crafting","energy":10},"gate":{"ingredients":{"steel-plate":2,"electronic-circuit":2,"stone-wall":1},"products":{"gate":1},"category":"crafting","energy":0.5},"green-wire":{"ingredients":{"copper-cable":1,"electronic-circuit":1},"products":{"green-wire":1},"category":"crafting","energy":0.5},"grenade":{"ingredients":{"coal":10,"iron-plate":5},"products":{"grenade":1},"category":"crafting","energy":8},"gun-turret":{"ingredients":{"iron-plate":20,"copper-plate":10,"iron-gear-wheel":10},"products":{"gun-turret":1},"category":"crafting","energy":8},"hazard-concrete":{"ingredients":{"concrete":10},"products":{"hazard-concrete":10},"category":"crafting","energy":0.25},"heat-exchanger":{"ingredients":{"copper-plate":100,"steel-plate":10,"pipe":10},"products":{"heat-exchanger":1},"category":"crafting","energy":3},"heat-pipe":{"ingredients":{"copper-plate":20,"steel-plate":10},"products":{"heat-pipe":1},"category":"crafting","energy":1},"heavy-armor":{"ingredients":{"copper-plate":100,"steel-plate":50},"products":{"heavy-armor":1},"category":"crafting","energy":8},"inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"electronic-circuit":1},"products":{"inserter":1},"category":"crafting","energy":0.5},"iron-chest":{"ingredients":{"iron-plate":8},"products":{"iron-chest":1},"category":"crafting","energy":0.5},"iron-gear-wheel":{"ingredients":{"iron-plate":2},"products":{"iron-gear-wheel":1},"category":"crafting","energy":0.5},"iron-plate":{"ingredients":{"iron-ore":1},"products":{"iron-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"iron-stick":{"ingredients":{"iron-plate":1},"products":{"iron-stick":2},"category":"crafting","energy":0.5},"lab":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"transport-belt":4},"products":{"lab":1},"category":"crafting","energy":2},"land-mine":{"ingredients":{"steel-plate":1,"explosives":2},"products":{"land-mine":4},"category":"crafting","energy":5},"landfill":{"ingredients":{"stone":20},"products":{"landfill":1},"category":"crafting","energy":0.5},"laser-turret":{"ingredients":{"steel-plate":20,"battery":12,"electronic-circuit":20},"products":{"laser-turret":1},"category":"crafting","energy":20},"light-armor":{"ingredients":{"iron-plate":40},"products":{"light-armor":1},"category":"crafting","energy":3},"locomotive":{"ingredients":{"steel-plate":30,"electronic-circuit":10,"engine-unit":20},"products":{"locomotive":1},"category":"crafting","energy":4},"logistic-chest-active-provider":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-active-provider":1},"category":"crafting","energy":0.5},"logistic-chest-buffer":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-buffer":1},"category":"crafting","energy":0.5},"logistic-chest-passive-provider":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-passive-provider":1},"category":"crafting","energy":0.5},"logistic-chest-requester":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-requester":1},"category":"crafting","energy":0.5},"logistic-chest-storage":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-storage":1},"category":"crafting","energy":0.5},"logistic-robot":{"ingredients":{"advanced-circuit":2,"flying-robot-frame":1},"products":{"logistic-robot":1},"category":"crafting","energy":0.5},"logistic-science-pack":{"ingredients":{"transport-belt":1,"inserter":1},"products":{"logistic-science-pack":1},"category":"crafting","energy":6},"long-handed-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"inserter":1},"products":{"long-handed-inserter":1},"category":"crafting","energy":0.5},"low-density-structure":{"ingredients":{"copper-plate":20,"steel-plate":2,"plastic-bar":5},"products":{"low-density-structure":1},"category":"crafting","energy":20},"lubricant":{"ingredients":{"heavy-oil":10},"products":{"lubricant":10},"category":"chemistry","energy":1},"medium-electric-pole":{"ingredients":{"copper-plate":2,"steel-plate":2,"iron-stick":4},"products":{"medium-electric-pole":1},"category":"crafting","energy":0.5},"military-science-pack":{"ingredients":{"piercing-rounds-magazine":1,"grenade":1,"stone-wall":2},"products":{"military-science-pack":2},"category":"crafting","energy":10},"modular-armor":{"ingredients":{"steel-plate":50,"advanced-circuit":30},"products":{"modular-armor":1},"category":"crafting","energy":15},"night-vision-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"night-vision-equipment":1},"category":"crafting","energy":10},"nuclear-fuel":{"ingredients":{"rocket-fuel":1,"uranium-235":1},"products":{"nuclear-fuel":1},"category":"centrifuging","energy":90},"nuclear-reactor":{"ingredients":{"copper-plate":500,"steel-plate":500,"advanced-circuit":500,"concrete":500},"products":{"nuclear-reactor":1},"category":"crafting","energy":8},"offshore-pump":{"ingredients":{"iron-gear-wheel":1,"electronic-circuit":2,"pipe":1},"products":{"offshore-pump":1},"category":"crafting","energy":0.5},"oil-refinery":{"ingredients":{"steel-plate":15,"iron-gear-wheel":10,"electronic-circuit":10,"pipe":10,"stone-brick":10},"products":{"oil-refinery":1},"category":"crafting","energy":8},"personal-laser-defense-equipment":{"ingredients":{"processing-unit":20,"low-density-structure":5,"laser-turret":5},"products":{"personal-laser-defense-equipment":1},"category":"crafting","energy":10},"personal-roboport-equipment":{"ingredients":{"steel-plate":20,"battery":45,"iron-gear-wheel":40,"advanced-circuit":10},"products":{"personal-roboport-equipment":1},"category":"crafting","energy":10},"personal-roboport-mk2-equipment":{"ingredients":{"processing-unit":100,"low-density-structure":20,"personal-roboport-equipment":5},"products":{"personal-roboport-mk2-equipment":1},"category":"crafting","energy":20},"piercing-rounds-magazine":{"ingredients":{"copper-plate":5,"steel-plate":1,"firearm-magazine":1},"products":{"piercing-rounds-magazine":1},"category":"crafting","energy":3},"piercing-shotgun-shell":{"ingredients":{"copper-plate":5,"steel-plate":2,"shotgun-shell":2},"products":{"piercing-shotgun-shell":1},"category":"crafting","energy":8},"pipe":{"ingredients":{"iron-plate":1},"products":{"pipe":1},"category":"crafting","energy":0.5},"pipe-to-ground":{"ingredients":{"iron-plate":5,"pipe":10},"products":{"pipe-to-ground":2},"category":"crafting","energy":0.5},"pistol":{"ingredients":{"iron-plate":5,"copper-plate":5},"products":{"pistol":1},"category":"crafting","energy":5},"plastic-bar":{"ingredients":{"coal":1,"petroleum-gas":20},"products":{"plastic-bar":2},"category":"chemistry","energy":1},"poison-capsule":{"ingredients":{"coal":10,"steel-plate":3,"electronic-circuit":3},"products":{"poison-capsule":1},"category":"crafting","energy":8},"power-armor":{"ingredients":{"steel-plate":40,"processing-unit":40,"electric-engine-unit":20},"products":{"power-armor":1},"category":"crafting","energy":20},"power-armor-mk2":{"ingredients":{"processing-unit":60,"electric-engine-unit":40,"low-density-structure":30,"speed-module-2":25,"effectivity-module-2":25},"products":{"power-armor-mk2":1},"category":"crafting","energy":25},"power-switch":{"ingredients":{"iron-plate":5,"copper-cable":5,"electronic-circuit":2},"products":{"power-switch":1},"category":"crafting","energy":2},"processing-unit":{"ingredients":{"electronic-circuit":20,"advanced-circuit":2,"sulfuric-acid":5},"products":{"processing-unit":1},"category":"crafting-with-fluid","energy":10},"production-science-pack":{"ingredients":{"rail":30,"electric-furnace":1,"productivity-module":1},"products":{"production-science-pack":3},"category":"crafting","energy":21},"productivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"productivity-module":1},"category":"crafting","energy":15},"productivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module":4},"products":{"productivity-module-2":1},"category":"crafting","energy":30},"productivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module-2":5},"products":{"productivity-module-3":1},"category":"crafting","energy":60},"programmable-speaker":{"ingredients":{"iron-plate":3,"copper-cable":5,"iron-stick":4,"electronic-circuit":4},"products":{"programmable-speaker":1},"category":"crafting","energy":2},"pump":{"ingredients":{"steel-plate":1,"engine-unit":1,"pipe":1},"products":{"pump":1},"category":"crafting","energy":2},"pumpjack":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10,"electronic-circuit":5,"pipe":10},"products":{"pumpjack":1},"category":"crafting","energy":5},"radar":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"radar":1},"category":"crafting","energy":0.5},"rail":{"ingredients":{"stone":1,"steel-plate":1,"iron-stick":1},"products":{"rail":2},"category":"crafting","energy":0.5},"rail-chain-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-chain-signal":1},"category":"crafting","energy":0.5},"rail-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-signal":1},"category":"crafting","energy":0.5},"red-wire":{"ingredients":{"copper-cable":1,"electronic-circuit":1},"products":{"red-wire":1},"category":"crafting","energy":0.5},"refined-concrete":{"ingredients":{"steel-plate":1,"iron-stick":8,"concrete":20,"water":100},"products":{"refined-concrete":10},"category":"crafting-with-fluid","energy":15},"refined-hazard-concrete":{"ingredients":{"refined-concrete":10},"products":{"refined-hazard-concrete":10},"category":"crafting","energy":0.25},"repair-pack":{"ingredients":{"iron-gear-wheel":2,"electronic-circuit":2},"products":{"repair-pack":1},"category":"crafting","energy":0.5},"roboport":{"ingredients":{"steel-plate":45,"iron-gear-wheel":45,"advanced-circuit":45},"products":{"roboport":1},"category":"crafting","energy":5},"rocket":{"ingredients":{"iron-plate":2,"explosives":1,"electronic-circuit":1},"products":{"rocket":1},"category":"crafting","energy":8},"rocket-control-unit":{"ingredients":{"processing-unit":1,"speed-module":1},"products":{"rocket-control-unit":1},"category":"crafting","energy":30},"rocket-fuel":{"ingredients":{"solid-fuel":10,"light-oil":10},"products":{"rocket-fuel":1},"category":"crafting-with-fluid","energy":30},"rocket-launcher":{"ingredients":{"iron-plate":5,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"rocket-launcher":1},"category":"crafting","energy":10},"rocket-part":{"ingredients":{"rocket-control-unit":10,"low-density-structure":10,"rocket-fuel":10},"products":{"rocket-part":1},"category":"rocket-building","energy":3},"rocket-silo":{"ingredients":{"steel-plate":1000,"processing-unit":200,"electric-engine-unit":200,"pipe":100,"concrete":1000},"products":{"rocket-silo":1},"category":"crafting","energy":30},"satellite":{"ingredients":{"processing-unit":100,"low-density-structure":100,"rocket-fuel":50,"solar-panel":100,"accumulator":100,"radar":5},"products":{"satellite":1},"category":"crafting","energy":5},"shotgun":{"ingredients":{"wood":5,"iron-plate":15,"copper-plate":10,"iron-gear-wheel":5},"products":{"shotgun":1},"category":"crafting","energy":10},"shotgun-shell":{"ingredients":{"iron-plate":2,"copper-plate":2},"products":{"shotgun-shell":1},"category":"crafting","energy":3},"slowdown-capsule":{"ingredients":{"coal":5,"steel-plate":2,"electronic-circuit":2},"products":{"slowdown-capsule":1},"category":"crafting","energy":8},"small-electric-pole":{"ingredients":{"wood":1,"copper-cable":2},"products":{"small-electric-pole":2},"category":"crafting","energy":0.5},"small-lamp":{"ingredients":{"iron-plate":1,"copper-cable":3,"electronic-circuit":1},"products":{"small-lamp":1},"category":"crafting","energy":0.5},"solar-panel":{"ingredients":{"copper-plate":5,"steel-plate":5,"electronic-circuit":15},"products":{"solar-panel":1},"category":"crafting","energy":10},"solar-panel-equipment":{"ingredients":{"steel-plate":5,"advanced-circuit":2,"solar-panel":1},"products":{"solar-panel-equipment":1},"category":"crafting","energy":10},"speed-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"speed-module":1},"category":"crafting","energy":15},"speed-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module":4},"products":{"speed-module-2":1},"category":"crafting","energy":30},"speed-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module-2":5},"products":{"speed-module-3":1},"category":"crafting","energy":60},"spidertron":{"ingredients":{"raw-fish":1,"rocket-control-unit":16,"low-density-structure":150,"effectivity-module-3":2,"rocket-launcher":4,"fusion-reactor-equipment":2,"exoskeleton-equipment":4,"radar":2},"products":{"spidertron":1},"category":"crafting","energy":10},"spidertron-remote":{"ingredients":{"rocket-control-unit":1,"radar":1},"products":{"spidertron-remote":1},"category":"crafting","energy":0.5},"splitter":{"ingredients":{"iron-plate":5,"electronic-circuit":5,"transport-belt":4},"products":{"splitter":1},"category":"crafting","energy":1},"stack-filter-inserter":{"ingredients":{"electronic-circuit":5,"stack-inserter":1},"products":{"stack-filter-inserter":1},"category":"crafting","energy":0.5},"stack-inserter":{"ingredients":{"iron-gear-wheel":15,"electronic-circuit":15,"advanced-circuit":1,"fast-inserter":1},"products":{"stack-inserter":1},"category":"crafting","energy":0.5},"steam-engine":{"ingredients":{"iron-plate":10,"iron-gear-wheel":8,"pipe":5},"products":{"steam-engine":1},"category":"crafting","energy":0.5},"steam-turbine":{"ingredients":{"copper-plate":50,"iron-gear-wheel":50,"pipe":20},"products":{"steam-turbine":1},"category":"crafting","energy":3},"steel-chest":{"ingredients":{"steel-plate":8},"products":{"steel-chest":1},"category":"crafting","energy":0.5},"steel-furnace":{"ingredients":{"steel-plate":6,"stone-brick":10},"products":{"steel-furnace":1},"category":"crafting","energy":3},"steel-plate":{"ingredients":{"iron-plate":5},"products":{"steel-plate":1},"category":"smelting","energy":16},"stone-brick":{"ingredients":{"stone":2},"products":{"stone-brick":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"stone-furnace":{"ingredients":{"stone":5},"products":{"stone-furnace":1},"category":"crafting","energy":0.5},"stone-wall":{"ingredients":{"stone-brick":5},"products":{"stone-wall":1},"category":"crafting","energy":0.5},"storage-tank":{"ingredients":{"iron-plate":20,"steel-plate":5},"products":{"storage-tank":1},"category":"crafting","energy":3},"submachine-gun":{"ingredients":{"iron-plate":10,"copper-plate":5,"iron-gear-wheel":10},"products":{"submachine-gun":1},"category":"crafting","energy":10},"substation":{"ingredients":{"copper-plate":5,"steel-plate":10,"advanced-circuit":5},"products":{"substation":1},"category":"crafting","energy":0.5},"sulfur":{"ingredients":{"water":30,"petroleum-gas":30},"products":{"sulfur":2},"category":"chemistry","energy":1},"sulfuric-acid":{"ingredients":{"iron-plate":1,"sulfur":5,"water":100},"products":{"sulfuric-acid":50},"category":"chemistry","energy":1},"tank":{"ingredients":{"steel-plate":50,"iron-gear-wheel":15,"advanced-circuit":10,"engine-unit":32},"products":{"tank":1},"category":"crafting","energy":5},"train-stop":{"ingredients":{"iron-plate":6,"steel-plate":3,"iron-stick":6,"electronic-circuit":5},"products":{"train-stop":1},"category":"crafting","energy":0.5},"transport-belt":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"transport-belt":2},"category":"crafting","energy":0.5},"underground-belt":{"ingredients":{"iron-plate":10,"transport-belt":5},"products":{"underground-belt":2},"category":"crafting","energy":1},"uranium-cannon-shell":{"ingredients":{"uranium-238":1,"cannon-shell":1},"products":{"uranium-cannon-shell":1},"category":"crafting","energy":12},"uranium-fuel-cell":{"ingredients":{"iron-plate":10,"uranium-235":1,"uranium-238":19},"products":{"uranium-fuel-cell":10},"category":"crafting","energy":10},"uranium-rounds-magazine":{"ingredients":{"uranium-238":1,"piercing-rounds-magazine":1},"products":{"uranium-rounds-magazine":1},"category":"crafting","energy":10},"utility-science-pack":{"ingredients":{"processing-unit":2,"flying-robot-frame":1,"low-density-structure":3},"products":{"utility-science-pack":3},"category":"crafting","energy":21},"wooden-chest":{"ingredients":{"wood":2},"products":{"wooden-chest":1},"category":"crafting","energy":0.5},"basic-oil-processing":{"ingredients":{"crude-oil":100},"products":{"petroleum-gas":45},"category":"oil-processing","energy":5},"advanced-oil-processing":{"ingredients":{"water":50,"crude-oil":100},"products":{"heavy-oil":25,"light-oil":45,"petroleum-gas":55},"category":"oil-processing","energy":5},"coal-liquefaction":{"ingredients":{"coal":10,"heavy-oil":25,"steam":50},"products":{"heavy-oil":90,"light-oil":20,"petroleum-gas":10},"category":"oil-processing","energy":5},"fill-crude-oil-barrel":{"ingredients":{"empty-barrel":1,"crude-oil":50},"products":{"crude-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-heavy-oil-barrel":{"ingredients":{"empty-barrel":1,"heavy-oil":50},"products":{"heavy-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-light-oil-barrel":{"ingredients":{"empty-barrel":1,"light-oil":50},"products":{"light-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-lubricant-barrel":{"ingredients":{"empty-barrel":1,"lubricant":50},"products":{"lubricant-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-petroleum-gas-barrel":{"ingredients":{"empty-barrel":1,"petroleum-gas":50},"products":{"petroleum-gas-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-sulfuric-acid-barrel":{"ingredients":{"empty-barrel":1,"sulfuric-acid":50},"products":{"sulfuric-acid-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-water-barrel":{"ingredients":{"empty-barrel":1,"water":50},"products":{"water-barrel":1},"category":"crafting-with-fluid","energy":0.2},"heavy-oil-cracking":{"ingredients":{"water":30,"heavy-oil":40},"products":{"light-oil":30},"category":"chemistry","energy":2},"light-oil-cracking":{"ingredients":{"water":30,"light-oil":30},"products":{"petroleum-gas":20},"category":"chemistry","energy":2},"solid-fuel-from-light-oil":{"ingredients":{"light-oil":10},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"solid-fuel-from-petroleum-gas":{"ingredients":{"petroleum-gas":20},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"solid-fuel-from-heavy-oil":{"ingredients":{"heavy-oil":20},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"empty-crude-oil-barrel":{"ingredients":{"crude-oil-barrel":1},"products":{"empty-barrel":1,"crude-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-heavy-oil-barrel":{"ingredients":{"heavy-oil-barrel":1},"products":{"empty-barrel":1,"heavy-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-light-oil-barrel":{"ingredients":{"light-oil-barrel":1},"products":{"empty-barrel":1,"light-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-lubricant-barrel":{"ingredients":{"lubricant-barrel":1},"products":{"empty-barrel":1,"lubricant":50},"category":"crafting-with-fluid","energy":0.2},"empty-petroleum-gas-barrel":{"ingredients":{"petroleum-gas-barrel":1},"products":{"empty-barrel":1,"petroleum-gas":50},"category":"crafting-with-fluid","energy":0.2},"empty-sulfuric-acid-barrel":{"ingredients":{"sulfuric-acid-barrel":1},"products":{"empty-barrel":1,"sulfuric-acid":50},"category":"crafting-with-fluid","energy":0.2},"empty-water-barrel":{"ingredients":{"water-barrel":1},"products":{"empty-barrel":1,"water":50},"category":"crafting-with-fluid","energy":0.2},"uranium-processing":{"ingredients":{"uranium-ore":10},"products":{"uranium-235":1,"uranium-238":1},"category":"centrifuging","energy":12},"nuclear-fuel-reprocessing":{"ingredients":{"used-up-uranium-fuel-cell":5},"products":{"uranium-238":3},"category":"centrifuging","energy":60},"kovarex-enrichment-process":{"ingredients":{"uranium-235":40,"uranium-238":5},"products":{"uranium-235":41,"uranium-238":2},"category":"centrifuging","energy":60}} \ No newline at end of file +{"wooden-chest":{"ingredients":{"wood":2},"products":{"wooden-chest":1},"category":"crafting","energy":0.5},"iron-chest":{"ingredients":{"iron-plate":8},"products":{"iron-chest":1},"category":"crafting","energy":0.5},"steel-chest":{"ingredients":{"steel-plate":8},"products":{"steel-chest":1},"category":"crafting","energy":0.5},"storage-tank":{"ingredients":{"iron-plate":20,"steel-plate":5},"products":{"storage-tank":1},"category":"crafting","energy":3},"transport-belt":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"transport-belt":2},"category":"crafting","energy":0.5},"fast-transport-belt":{"ingredients":{"iron-gear-wheel":5,"transport-belt":1},"products":{"fast-transport-belt":1},"category":"crafting","energy":0.5},"express-transport-belt":{"ingredients":{"iron-gear-wheel":10,"fast-transport-belt":1,"lubricant":20},"products":{"express-transport-belt":1},"category":"crafting-with-fluid","energy":0.5},"underground-belt":{"ingredients":{"iron-plate":10,"transport-belt":5},"products":{"underground-belt":2},"category":"crafting","energy":1},"fast-underground-belt":{"ingredients":{"iron-gear-wheel":40,"underground-belt":2},"products":{"fast-underground-belt":2},"category":"crafting","energy":2},"express-underground-belt":{"ingredients":{"iron-gear-wheel":80,"fast-underground-belt":2,"lubricant":40},"products":{"express-underground-belt":2},"category":"crafting-with-fluid","energy":2},"splitter":{"ingredients":{"iron-plate":5,"electronic-circuit":5,"transport-belt":4},"products":{"splitter":1},"category":"crafting","energy":1},"fast-splitter":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"splitter":1},"products":{"fast-splitter":1},"category":"crafting","energy":2},"express-splitter":{"ingredients":{"iron-gear-wheel":10,"advanced-circuit":10,"fast-splitter":1,"lubricant":80},"products":{"express-splitter":1},"category":"crafting-with-fluid","energy":2},"burner-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"burner-inserter":1},"category":"crafting","energy":0.5},"inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"electronic-circuit":1},"products":{"inserter":1},"category":"crafting","energy":0.5},"long-handed-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"inserter":1},"products":{"long-handed-inserter":1},"category":"crafting","energy":0.5},"fast-inserter":{"ingredients":{"iron-plate":2,"electronic-circuit":2,"inserter":1},"products":{"fast-inserter":1},"category":"crafting","energy":0.5},"bulk-inserter":{"ingredients":{"iron-gear-wheel":15,"electronic-circuit":15,"advanced-circuit":1,"fast-inserter":1},"products":{"bulk-inserter":1},"category":"crafting","energy":0.5},"small-electric-pole":{"ingredients":{"wood":1,"copper-cable":2},"products":{"small-electric-pole":2},"category":"crafting","energy":0.5},"medium-electric-pole":{"ingredients":{"steel-plate":2,"iron-stick":4,"copper-cable":2},"products":{"medium-electric-pole":1},"category":"crafting","energy":0.5},"big-electric-pole":{"ingredients":{"steel-plate":5,"iron-stick":8,"copper-cable":4},"products":{"big-electric-pole":1},"category":"crafting","energy":0.5},"substation":{"ingredients":{"steel-plate":10,"copper-cable":6,"advanced-circuit":5},"products":{"substation":1},"category":"crafting","energy":0.5},"pipe":{"ingredients":{"iron-plate":1},"products":{"pipe":1},"category":"crafting","energy":0.5},"pipe-to-ground":{"ingredients":{"iron-plate":5,"pipe":10},"products":{"pipe-to-ground":2},"category":"crafting","energy":0.5},"pump":{"ingredients":{"steel-plate":1,"engine-unit":1,"pipe":1},"products":{"pump":1},"category":"crafting","energy":2},"rail":{"ingredients":{"stone":1,"steel-plate":1,"iron-stick":1},"products":{"rail":2},"category":"crafting","energy":0.5},"train-stop":{"ingredients":{"iron-plate":6,"steel-plate":3,"iron-stick":6,"electronic-circuit":5},"products":{"train-stop":1},"category":"crafting","energy":0.5},"rail-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-signal":1},"category":"crafting","energy":0.5},"rail-chain-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-chain-signal":1},"category":"crafting","energy":0.5},"locomotive":{"ingredients":{"steel-plate":30,"electronic-circuit":10,"engine-unit":20},"products":{"locomotive":1},"category":"crafting","energy":4},"cargo-wagon":{"ingredients":{"iron-plate":20,"steel-plate":20,"iron-gear-wheel":10},"products":{"cargo-wagon":1},"category":"crafting","energy":1},"fluid-wagon":{"ingredients":{"steel-plate":16,"iron-gear-wheel":10,"storage-tank":1,"pipe":8},"products":{"fluid-wagon":1},"category":"crafting","energy":1.5},"artillery-wagon":{"ingredients":{"steel-plate":40,"iron-gear-wheel":10,"advanced-circuit":20,"engine-unit":64,"pipe":16},"products":{"artillery-wagon":1},"category":"crafting","energy":4},"car":{"ingredients":{"iron-plate":20,"steel-plate":5,"engine-unit":8},"products":{"car":1},"category":"crafting","energy":2},"tank":{"ingredients":{"steel-plate":50,"iron-gear-wheel":15,"advanced-circuit":10,"engine-unit":32},"products":{"tank":1},"category":"crafting","energy":5},"spidertron":{"ingredients":{"raw-fish":1,"processing-unit":16,"low-density-structure":150,"efficiency-module-3":2,"rocket-launcher":4,"fission-reactor-equipment":2,"exoskeleton-equipment":4,"radar":2},"products":{"spidertron":1},"category":"crafting","energy":10},"logistic-robot":{"ingredients":{"advanced-circuit":2,"flying-robot-frame":1},"products":{"logistic-robot":1},"category":"crafting","energy":0.5},"construction-robot":{"ingredients":{"electronic-circuit":2,"flying-robot-frame":1},"products":{"construction-robot":1},"category":"crafting","energy":0.5},"active-provider-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"active-provider-chest":1},"category":"crafting","energy":0.5},"passive-provider-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"passive-provider-chest":1},"category":"crafting","energy":0.5},"storage-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"storage-chest":1},"category":"crafting","energy":0.5},"buffer-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"buffer-chest":1},"category":"crafting","energy":0.5},"requester-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"requester-chest":1},"category":"crafting","energy":0.5},"roboport":{"ingredients":{"steel-plate":45,"iron-gear-wheel":45,"advanced-circuit":45},"products":{"roboport":1},"category":"crafting","energy":5},"small-lamp":{"ingredients":{"iron-plate":1,"copper-cable":3,"electronic-circuit":1},"products":{"small-lamp":1},"category":"crafting","energy":0.5},"arithmetic-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"arithmetic-combinator":1},"category":"crafting","energy":0.5},"decider-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"decider-combinator":1},"category":"crafting","energy":0.5},"selector-combinator":{"ingredients":{"advanced-circuit":2,"decider-combinator":5},"products":{"selector-combinator":1},"category":"crafting","energy":0.5},"constant-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":2},"products":{"constant-combinator":1},"category":"crafting","energy":0.5},"power-switch":{"ingredients":{"iron-plate":5,"copper-cable":5,"electronic-circuit":2},"products":{"power-switch":1},"category":"crafting","energy":2},"programmable-speaker":{"ingredients":{"iron-plate":3,"iron-stick":4,"copper-cable":5,"electronic-circuit":4},"products":{"programmable-speaker":1},"category":"crafting","energy":2},"display-panel":{"ingredients":{"iron-plate":1,"electronic-circuit":1},"products":{"display-panel":1},"category":"crafting","energy":0.5},"stone-brick":{"ingredients":{"stone":2},"products":{"stone-brick":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"concrete":{"ingredients":{"iron-ore":1,"stone-brick":5,"water":100},"products":{"concrete":10},"category":"crafting-with-fluid","energy":10},"hazard-concrete":{"ingredients":{"concrete":10},"products":{"hazard-concrete":10},"category":"crafting","energy":0.25},"refined-concrete":{"ingredients":{"steel-plate":1,"iron-stick":8,"concrete":20,"water":100},"products":{"refined-concrete":10},"category":"crafting-with-fluid","energy":15},"refined-hazard-concrete":{"ingredients":{"refined-concrete":10},"products":{"refined-hazard-concrete":10},"category":"crafting","energy":0.25},"landfill":{"ingredients":{"stone":50},"products":{"landfill":1},"category":"crafting","energy":0.5},"cliff-explosives":{"ingredients":{"explosives":10,"barrel":1,"grenade":1},"products":{"cliff-explosives":1},"category":"crafting","energy":8},"repair-pack":{"ingredients":{"iron-gear-wheel":2,"electronic-circuit":2},"products":{"repair-pack":1},"category":"crafting","energy":0.5},"boiler":{"ingredients":{"pipe":4,"stone-furnace":1},"products":{"boiler":1},"category":"crafting","energy":0.5},"steam-engine":{"ingredients":{"iron-plate":10,"iron-gear-wheel":8,"pipe":5},"products":{"steam-engine":1},"category":"crafting","energy":0.5},"solar-panel":{"ingredients":{"copper-plate":5,"steel-plate":5,"electronic-circuit":15},"products":{"solar-panel":1},"category":"crafting","energy":10},"accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting","energy":10},"nuclear-reactor":{"ingredients":{"copper-plate":500,"steel-plate":500,"advanced-circuit":500,"concrete":500},"products":{"nuclear-reactor":1},"category":"crafting","energy":8},"heat-pipe":{"ingredients":{"copper-plate":20,"steel-plate":10},"products":{"heat-pipe":1},"category":"crafting","energy":1},"heat-exchanger":{"ingredients":{"copper-plate":100,"steel-plate":10,"pipe":10},"products":{"heat-exchanger":1},"category":"crafting","energy":3},"steam-turbine":{"ingredients":{"copper-plate":50,"iron-gear-wheel":50,"pipe":20},"products":{"steam-turbine":1},"category":"crafting","energy":3},"burner-mining-drill":{"ingredients":{"iron-plate":3,"iron-gear-wheel":3,"stone-furnace":1},"products":{"burner-mining-drill":1},"category":"crafting","energy":2},"electric-mining-drill":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"electric-mining-drill":1},"category":"crafting","energy":2},"offshore-pump":{"ingredients":{"iron-gear-wheel":2,"pipe":3},"products":{"offshore-pump":1},"category":"crafting","energy":0.5},"pumpjack":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10,"electronic-circuit":5,"pipe":10},"products":{"pumpjack":1},"category":"crafting","energy":5},"stone-furnace":{"ingredients":{"stone":5},"products":{"stone-furnace":1},"category":"crafting","energy":0.5},"steel-furnace":{"ingredients":{"steel-plate":6,"stone-brick":10},"products":{"steel-furnace":1},"category":"crafting","energy":3},"electric-furnace":{"ingredients":{"steel-plate":10,"advanced-circuit":5,"stone-brick":10},"products":{"electric-furnace":1},"category":"crafting","energy":5},"assembling-machine-1":{"ingredients":{"iron-plate":9,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"assembling-machine-1":1},"category":"crafting","energy":0.5},"assembling-machine-2":{"ingredients":{"steel-plate":2,"iron-gear-wheel":5,"electronic-circuit":3,"assembling-machine-1":1},"products":{"assembling-machine-2":1},"category":"crafting","energy":0.5},"assembling-machine-3":{"ingredients":{"assembling-machine-2":2,"speed-module":4},"products":{"assembling-machine-3":1},"category":"crafting","energy":0.5},"oil-refinery":{"ingredients":{"steel-plate":15,"iron-gear-wheel":10,"electronic-circuit":10,"pipe":10,"stone-brick":10},"products":{"oil-refinery":1},"category":"crafting","energy":8},"chemical-plant":{"ingredients":{"steel-plate":5,"iron-gear-wheel":5,"electronic-circuit":5,"pipe":5},"products":{"chemical-plant":1},"category":"crafting","energy":5},"centrifuge":{"ingredients":{"steel-plate":50,"iron-gear-wheel":100,"advanced-circuit":100,"concrete":100},"products":{"centrifuge":1},"category":"crafting","energy":4},"lab":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"transport-belt":4},"products":{"lab":1},"category":"crafting","energy":2},"beacon":{"ingredients":{"steel-plate":10,"copper-cable":10,"electronic-circuit":20,"advanced-circuit":20},"products":{"beacon":1},"category":"crafting","energy":15},"speed-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"speed-module":1},"category":"crafting","energy":15},"speed-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module":4},"products":{"speed-module-2":1},"category":"crafting","energy":30},"speed-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module-2":4},"products":{"speed-module-3":1},"category":"crafting","energy":60},"efficiency-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"efficiency-module":1},"category":"crafting","energy":15},"efficiency-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"efficiency-module":4},"products":{"efficiency-module-2":1},"category":"crafting","energy":30},"efficiency-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"efficiency-module-2":4},"products":{"efficiency-module-3":1},"category":"crafting","energy":60},"productivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"productivity-module":1},"category":"crafting","energy":15},"productivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module":4},"products":{"productivity-module-2":1},"category":"crafting","energy":30},"productivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module-2":4},"products":{"productivity-module-3":1},"category":"crafting","energy":60},"rocket-silo":{"ingredients":{"steel-plate":1000,"processing-unit":200,"electric-engine-unit":200,"pipe":100,"concrete":1000},"products":{"rocket-silo":1},"category":"crafting","energy":30},"cargo-landing-pad":{"ingredients":{"steel-plate":25,"processing-unit":10,"concrete":200},"products":{"cargo-landing-pad":1},"category":"crafting","energy":30},"satellite":{"ingredients":{"processing-unit":100,"low-density-structure":100,"rocket-fuel":50,"solar-panel":100,"accumulator":100,"radar":5},"products":{"satellite":1},"category":"crafting","energy":5},"basic-oil-processing":{"ingredients":{"crude-oil":100},"products":{"petroleum-gas":45},"category":"oil-processing","energy":5},"advanced-oil-processing":{"ingredients":{"water":50,"crude-oil":100},"products":{"heavy-oil":25,"light-oil":45,"petroleum-gas":55},"category":"oil-processing","energy":5},"coal-liquefaction":{"ingredients":{"coal":10,"heavy-oil":25,"steam":50},"products":{"heavy-oil":90,"light-oil":20,"petroleum-gas":10},"category":"oil-processing","energy":5},"heavy-oil-cracking":{"ingredients":{"water":30,"heavy-oil":40},"products":{"light-oil":30},"category":"chemistry","energy":2},"light-oil-cracking":{"ingredients":{"water":30,"light-oil":30},"products":{"petroleum-gas":20},"category":"chemistry","energy":2},"solid-fuel-from-petroleum-gas":{"ingredients":{"petroleum-gas":20},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"solid-fuel-from-light-oil":{"ingredients":{"light-oil":10},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"solid-fuel-from-heavy-oil":{"ingredients":{"heavy-oil":20},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"lubricant":{"ingredients":{"heavy-oil":10},"products":{"lubricant":10},"category":"chemistry","energy":1},"sulfuric-acid":{"ingredients":{"iron-plate":1,"sulfur":5,"water":100},"products":{"sulfuric-acid":50},"category":"chemistry","energy":1},"iron-plate":{"ingredients":{"iron-ore":1},"products":{"iron-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"copper-plate":{"ingredients":{"copper-ore":1},"products":{"copper-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"steel-plate":{"ingredients":{"iron-plate":5},"products":{"steel-plate":1},"category":"smelting","energy":16},"plastic-bar":{"ingredients":{"coal":1,"petroleum-gas":20},"products":{"plastic-bar":2},"category":"chemistry","energy":1},"sulfur":{"ingredients":{"water":30,"petroleum-gas":30},"products":{"sulfur":2},"category":"chemistry","energy":1},"battery":{"ingredients":{"iron-plate":1,"copper-plate":1,"sulfuric-acid":20},"products":{"battery":1},"category":"chemistry","energy":4},"explosives":{"ingredients":{"coal":1,"sulfur":1,"water":10},"products":{"explosives":2},"category":"chemistry","energy":4},"water-barrel":{"ingredients":{"barrel":1,"water":50},"products":{"water-barrel":1},"category":"crafting-with-fluid","energy":0.2},"crude-oil-barrel":{"ingredients":{"barrel":1,"crude-oil":50},"products":{"crude-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"petroleum-gas-barrel":{"ingredients":{"barrel":1,"petroleum-gas":50},"products":{"petroleum-gas-barrel":1},"category":"crafting-with-fluid","energy":0.2},"light-oil-barrel":{"ingredients":{"barrel":1,"light-oil":50},"products":{"light-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"heavy-oil-barrel":{"ingredients":{"barrel":1,"heavy-oil":50},"products":{"heavy-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"lubricant-barrel":{"ingredients":{"barrel":1,"lubricant":50},"products":{"lubricant-barrel":1},"category":"crafting-with-fluid","energy":0.2},"sulfuric-acid-barrel":{"ingredients":{"barrel":1,"sulfuric-acid":50},"products":{"sulfuric-acid-barrel":1},"category":"crafting-with-fluid","energy":0.2},"empty-water-barrel":{"ingredients":{"water-barrel":1},"products":{"barrel":1,"water":50},"category":"crafting-with-fluid","energy":0.2},"empty-crude-oil-barrel":{"ingredients":{"crude-oil-barrel":1},"products":{"barrel":1,"crude-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-petroleum-gas-barrel":{"ingredients":{"petroleum-gas-barrel":1},"products":{"barrel":1,"petroleum-gas":50},"category":"crafting-with-fluid","energy":0.2},"empty-light-oil-barrel":{"ingredients":{"light-oil-barrel":1},"products":{"barrel":1,"light-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-heavy-oil-barrel":{"ingredients":{"heavy-oil-barrel":1},"products":{"barrel":1,"heavy-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-lubricant-barrel":{"ingredients":{"lubricant-barrel":1},"products":{"barrel":1,"lubricant":50},"category":"crafting-with-fluid","energy":0.2},"empty-sulfuric-acid-barrel":{"ingredients":{"sulfuric-acid-barrel":1},"products":{"barrel":1,"sulfuric-acid":50},"category":"crafting-with-fluid","energy":0.2},"iron-gear-wheel":{"ingredients":{"iron-plate":2},"products":{"iron-gear-wheel":1},"category":"crafting","energy":0.5},"iron-stick":{"ingredients":{"iron-plate":1},"products":{"iron-stick":2},"category":"crafting","energy":0.5},"copper-cable":{"ingredients":{"copper-plate":1},"products":{"copper-cable":2},"category":"crafting","energy":0.5},"barrel":{"ingredients":{"steel-plate":1},"products":{"barrel":1},"category":"crafting","energy":1},"electronic-circuit":{"ingredients":{"iron-plate":1,"copper-cable":3},"products":{"electronic-circuit":1},"category":"crafting","energy":0.5},"advanced-circuit":{"ingredients":{"plastic-bar":2,"copper-cable":4,"electronic-circuit":2},"products":{"advanced-circuit":1},"category":"crafting","energy":6},"processing-unit":{"ingredients":{"electronic-circuit":20,"advanced-circuit":2,"sulfuric-acid":5},"products":{"processing-unit":1},"category":"crafting-with-fluid","energy":10},"engine-unit":{"ingredients":{"steel-plate":1,"iron-gear-wheel":1,"pipe":2},"products":{"engine-unit":1},"category":"advanced-crafting","energy":10},"electric-engine-unit":{"ingredients":{"electronic-circuit":2,"engine-unit":1,"lubricant":15},"products":{"electric-engine-unit":1},"category":"crafting-with-fluid","energy":10},"flying-robot-frame":{"ingredients":{"steel-plate":1,"battery":2,"electronic-circuit":3,"electric-engine-unit":1},"products":{"flying-robot-frame":1},"category":"crafting","energy":20},"low-density-structure":{"ingredients":{"copper-plate":20,"steel-plate":2,"plastic-bar":5},"products":{"low-density-structure":1},"category":"crafting","energy":15},"rocket-fuel":{"ingredients":{"solid-fuel":10,"light-oil":10},"products":{"rocket-fuel":1},"category":"crafting-with-fluid","energy":15},"rocket-part":{"ingredients":{"processing-unit":10,"low-density-structure":10,"rocket-fuel":10},"products":{"rocket-part":1},"category":"rocket-building","energy":3},"uranium-processing":{"ingredients":{"uranium-ore":10},"products":{"uranium-235":1,"uranium-238":1},"category":"centrifuging","energy":12},"uranium-fuel-cell":{"ingredients":{"iron-plate":10,"uranium-235":1,"uranium-238":19},"products":{"uranium-fuel-cell":10},"category":"crafting","energy":10},"nuclear-fuel-reprocessing":{"ingredients":{"depleted-uranium-fuel-cell":5},"products":{"uranium-238":3},"category":"centrifuging","energy":60},"kovarex-enrichment-process":{"ingredients":{"uranium-235":40,"uranium-238":5},"products":{"uranium-235":41,"uranium-238":2},"category":"centrifuging","energy":60},"nuclear-fuel":{"ingredients":{"rocket-fuel":1,"uranium-235":1},"products":{"nuclear-fuel":1},"category":"centrifuging","energy":90},"automation-science-pack":{"ingredients":{"copper-plate":1,"iron-gear-wheel":1},"products":{"automation-science-pack":1},"category":"crafting","energy":5},"logistic-science-pack":{"ingredients":{"transport-belt":1,"inserter":1},"products":{"logistic-science-pack":1},"category":"crafting","energy":6},"military-science-pack":{"ingredients":{"piercing-rounds-magazine":1,"grenade":1,"stone-wall":2},"products":{"military-science-pack":2},"category":"crafting","energy":10},"chemical-science-pack":{"ingredients":{"sulfur":1,"advanced-circuit":3,"engine-unit":2},"products":{"chemical-science-pack":2},"category":"crafting","energy":24},"production-science-pack":{"ingredients":{"rail":30,"electric-furnace":1,"productivity-module":1},"products":{"production-science-pack":3},"category":"crafting","energy":21},"utility-science-pack":{"ingredients":{"processing-unit":2,"flying-robot-frame":1,"low-density-structure":3},"products":{"utility-science-pack":3},"category":"crafting","energy":21},"submachine-gun":{"ingredients":{"iron-plate":10,"copper-plate":5,"iron-gear-wheel":10},"products":{"submachine-gun":1},"category":"crafting","energy":10},"shotgun":{"ingredients":{"wood":5,"iron-plate":15,"copper-plate":10,"iron-gear-wheel":5},"products":{"shotgun":1},"category":"crafting","energy":10},"combat-shotgun":{"ingredients":{"wood":10,"copper-plate":10,"steel-plate":15,"iron-gear-wheel":5},"products":{"combat-shotgun":1},"category":"crafting","energy":10},"rocket-launcher":{"ingredients":{"iron-plate":5,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"rocket-launcher":1},"category":"crafting","energy":10},"flamethrower":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10},"products":{"flamethrower":1},"category":"crafting","energy":10},"firearm-magazine":{"ingredients":{"iron-plate":4},"products":{"firearm-magazine":1},"category":"crafting","energy":1},"piercing-rounds-magazine":{"ingredients":{"copper-plate":5,"steel-plate":1,"firearm-magazine":1},"products":{"piercing-rounds-magazine":1},"category":"crafting","energy":3},"uranium-rounds-magazine":{"ingredients":{"uranium-238":1,"piercing-rounds-magazine":1},"products":{"uranium-rounds-magazine":1},"category":"crafting","energy":10},"shotgun-shell":{"ingredients":{"iron-plate":2,"copper-plate":2},"products":{"shotgun-shell":1},"category":"crafting","energy":3},"piercing-shotgun-shell":{"ingredients":{"copper-plate":5,"steel-plate":2,"shotgun-shell":2},"products":{"piercing-shotgun-shell":1},"category":"crafting","energy":8},"cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":1},"products":{"cannon-shell":1},"category":"crafting","energy":8},"explosive-cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":2},"products":{"explosive-cannon-shell":1},"category":"crafting","energy":8},"uranium-cannon-shell":{"ingredients":{"uranium-238":1,"cannon-shell":1},"products":{"uranium-cannon-shell":1},"category":"crafting","energy":12},"explosive-uranium-cannon-shell":{"ingredients":{"uranium-238":1,"explosive-cannon-shell":1},"products":{"explosive-uranium-cannon-shell":1},"category":"crafting","energy":12},"artillery-shell":{"ingredients":{"explosives":8,"explosive-cannon-shell":4,"radar":1},"products":{"artillery-shell":1},"category":"crafting","energy":15},"rocket":{"ingredients":{"iron-plate":2,"explosives":1},"products":{"rocket":1},"category":"crafting","energy":4},"explosive-rocket":{"ingredients":{"explosives":2,"rocket":1},"products":{"explosive-rocket":1},"category":"crafting","energy":8},"atomic-bomb":{"ingredients":{"explosives":10,"processing-unit":10,"uranium-235":30},"products":{"atomic-bomb":1},"category":"crafting","energy":50},"flamethrower-ammo":{"ingredients":{"steel-plate":5,"crude-oil":100},"products":{"flamethrower-ammo":1},"category":"chemistry","energy":6},"grenade":{"ingredients":{"coal":10,"iron-plate":5},"products":{"grenade":1},"category":"crafting","energy":8},"cluster-grenade":{"ingredients":{"steel-plate":5,"explosives":5,"grenade":7},"products":{"cluster-grenade":1},"category":"crafting","energy":8},"poison-capsule":{"ingredients":{"coal":10,"steel-plate":3,"electronic-circuit":3},"products":{"poison-capsule":1},"category":"crafting","energy":8},"slowdown-capsule":{"ingredients":{"coal":5,"steel-plate":2,"electronic-circuit":2},"products":{"slowdown-capsule":1},"category":"crafting","energy":8},"defender-capsule":{"ingredients":{"iron-gear-wheel":3,"electronic-circuit":3,"piercing-rounds-magazine":3},"products":{"defender-capsule":1},"category":"crafting","energy":8},"distractor-capsule":{"ingredients":{"advanced-circuit":3,"defender-capsule":4},"products":{"distractor-capsule":1},"category":"crafting","energy":15},"destroyer-capsule":{"ingredients":{"speed-module":1,"distractor-capsule":4},"products":{"destroyer-capsule":1},"category":"crafting","energy":15},"light-armor":{"ingredients":{"iron-plate":40},"products":{"light-armor":1},"category":"crafting","energy":3},"heavy-armor":{"ingredients":{"copper-plate":100,"steel-plate":50},"products":{"heavy-armor":1},"category":"crafting","energy":8},"modular-armor":{"ingredients":{"steel-plate":50,"advanced-circuit":30},"products":{"modular-armor":1},"category":"crafting","energy":15},"power-armor":{"ingredients":{"steel-plate":40,"processing-unit":40,"electric-engine-unit":20},"products":{"power-armor":1},"category":"crafting","energy":20},"power-armor-mk2":{"ingredients":{"processing-unit":60,"electric-engine-unit":40,"low-density-structure":30,"speed-module-2":25,"efficiency-module-2":25},"products":{"power-armor-mk2":1},"category":"crafting","energy":25},"solar-panel-equipment":{"ingredients":{"steel-plate":5,"advanced-circuit":2,"solar-panel":1},"products":{"solar-panel-equipment":1},"category":"crafting","energy":10},"fission-reactor-equipment":{"ingredients":{"processing-unit":200,"low-density-structure":50,"uranium-fuel-cell":4},"products":{"fission-reactor-equipment":1},"category":"crafting","energy":10},"battery-equipment":{"ingredients":{"steel-plate":10,"battery":5},"products":{"battery-equipment":1},"category":"crafting","energy":10},"battery-mk2-equipment":{"ingredients":{"processing-unit":15,"low-density-structure":5,"battery-equipment":10},"products":{"battery-mk2-equipment":1},"category":"crafting","energy":10},"belt-immunity-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"belt-immunity-equipment":1},"category":"crafting","energy":10},"exoskeleton-equipment":{"ingredients":{"steel-plate":20,"processing-unit":10,"electric-engine-unit":30},"products":{"exoskeleton-equipment":1},"category":"crafting","energy":10},"personal-roboport-equipment":{"ingredients":{"steel-plate":20,"battery":45,"iron-gear-wheel":40,"advanced-circuit":10},"products":{"personal-roboport-equipment":1},"category":"crafting","energy":10},"personal-roboport-mk2-equipment":{"ingredients":{"processing-unit":100,"low-density-structure":20,"personal-roboport-equipment":5},"products":{"personal-roboport-mk2-equipment":1},"category":"crafting","energy":20},"night-vision-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"night-vision-equipment":1},"category":"crafting","energy":10},"energy-shield-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"energy-shield-equipment":1},"category":"crafting","energy":10},"energy-shield-mk2-equipment":{"ingredients":{"processing-unit":5,"low-density-structure":5,"energy-shield-equipment":10},"products":{"energy-shield-mk2-equipment":1},"category":"crafting","energy":10},"personal-laser-defense-equipment":{"ingredients":{"processing-unit":20,"low-density-structure":5,"laser-turret":5},"products":{"personal-laser-defense-equipment":1},"category":"crafting","energy":10},"discharge-defense-equipment":{"ingredients":{"steel-plate":20,"processing-unit":5,"laser-turret":10},"products":{"discharge-defense-equipment":1},"category":"crafting","energy":10},"stone-wall":{"ingredients":{"stone-brick":5},"products":{"stone-wall":1},"category":"crafting","energy":0.5},"gate":{"ingredients":{"steel-plate":2,"electronic-circuit":2,"stone-wall":1},"products":{"gate":1},"category":"crafting","energy":0.5},"radar":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"radar":1},"category":"crafting","energy":0.5},"land-mine":{"ingredients":{"steel-plate":1,"explosives":2},"products":{"land-mine":4},"category":"crafting","energy":5},"gun-turret":{"ingredients":{"iron-plate":20,"copper-plate":10,"iron-gear-wheel":10},"products":{"gun-turret":1},"category":"crafting","energy":8},"laser-turret":{"ingredients":{"steel-plate":20,"battery":12,"electronic-circuit":20},"products":{"laser-turret":1},"category":"crafting","energy":20},"flamethrower-turret":{"ingredients":{"steel-plate":30,"iron-gear-wheel":15,"engine-unit":5,"pipe":10},"products":{"flamethrower-turret":1},"category":"crafting","energy":20},"artillery-turret":{"ingredients":{"steel-plate":60,"iron-gear-wheel":40,"advanced-circuit":20,"concrete":60},"products":{"artillery-turret":1},"category":"crafting","energy":40},"parameter-0":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-1":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-2":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-3":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-4":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-5":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-6":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-7":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-8":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-9":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"recipe-unknown":{"ingredients":{},"products":{},"category":"crafting","energy":0.5}} \ No newline at end of file diff --git a/worlds/factorio/data/resources.json b/worlds/factorio/data/resources.json index 10279db37955..80c00fe3df42 100644 --- a/worlds/factorio/data/resources.json +++ b/worlds/factorio/data/resources.json @@ -1 +1 @@ -{"iron-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"iron-ore":{"name":"iron-ore","amount":1}}},"copper-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"copper-ore":{"name":"copper-ore","amount":1}}},"stone":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"stone":{"name":"stone","amount":1}}},"coal":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"coal":{"name":"coal","amount":1}}},"uranium-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":2,"required_fluid":"sulfuric-acid","fluid_amount":10,"products":{"uranium-ore":{"name":"uranium-ore","amount":1}}},"crude-oil":{"minable":true,"infinite":true,"infinite_depletion":10,"category":"basic-fluid","mining_time":1,"products":{"crude-oil":{"name":"crude-oil","amount":10}}}} \ No newline at end of file +{"iron-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"iron-ore":{"name":"iron-ore","amount":1}}},"copper-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"copper-ore":{"name":"copper-ore","amount":1}}},"stone":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"stone":{"name":"stone","amount":1}}},"coal":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"coal":{"name":"coal","amount":1}}},"crude-oil":{"minable":true,"infinite":true,"infinite_depletion":10,"category":"basic-fluid","mining_time":1,"products":{"crude-oil":{"name":"crude-oil","amount":10}}},"uranium-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":2,"required_fluid":"sulfuric-acid","fluid_amount":10,"products":{"uranium-ore":{"name":"uranium-ore","amount":1}}}} \ No newline at end of file diff --git a/worlds/factorio/data/techs.json b/worlds/factorio/data/techs.json index d9977f2986d6..ecb31126e1dc 100644 --- a/worlds/factorio/data/techs.json +++ b/worlds/factorio/data/techs.json @@ -1 +1 @@ -{"automation":{"unlocks":["assembling-machine-1","long-handed-inserter"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"automation-2":{"unlocks":["assembling-machine-2"],"requires":["electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"automation-3":{"unlocks":["assembling-machine-3"],"requires":["speed-module","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"electronics":{"unlocks":{},"requires":["automation"],"ingredients":["automation-science-pack"],"has_modifier":false},"fast-inserter":{"unlocks":["fast-inserter","filter-inserter"],"requires":["electronics"],"ingredients":["automation-science-pack"],"has_modifier":false},"advanced-electronics":{"unlocks":["advanced-circuit"],"requires":["plastics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"advanced-electronics-2":{"unlocks":["processing-unit"],"requires":["chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"circuit-network":{"unlocks":["red-wire","green-wire","arithmetic-combinator","decider-combinator","constant-combinator","power-switch","programmable-speaker"],"requires":["electronics","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"explosives":{"unlocks":["explosives"],"requires":["sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistics":{"unlocks":["underground-belt","splitter"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"logistics-2":{"unlocks":["fast-transport-belt","fast-underground-belt","fast-splitter"],"requires":["logistics","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistics-3":{"unlocks":["express-transport-belt","express-underground-belt","express-splitter"],"requires":["production-science-pack","lubricant"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"optics":{"unlocks":["small-lamp"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"laser":{"unlocks":{},"requires":["optics","battery","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"solar-energy":{"unlocks":["solar-panel"],"requires":["optics","electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"gun-turret":{"unlocks":["gun-turret"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"laser-turret":{"unlocks":["laser-turret"],"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"stone-wall":{"unlocks":["stone-wall"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"gate":{"unlocks":["gate"],"requires":["stone-wall","military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"engine":{"unlocks":["engine-unit"],"requires":["steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-engine":{"unlocks":["electric-engine-unit"],"requires":["lubricant"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"lubricant":{"unlocks":["lubricant"],"requires":["advanced-oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"battery":{"unlocks":["battery"],"requires":["sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"landfill":{"unlocks":["landfill"],"requires":["logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"braking-force-1":{"unlocks":{},"requires":["railway","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"braking-force-2":{"unlocks":{},"requires":["braking-force-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"braking-force-3":{"unlocks":{},"requires":["braking-force-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-4":{"unlocks":{},"requires":["braking-force-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-5":{"unlocks":{},"requires":["braking-force-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-6":{"unlocks":{},"requires":["braking-force-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"braking-force-7":{"unlocks":{},"requires":["braking-force-6"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"chemical-science-pack":{"unlocks":["chemical-science-pack"],"requires":["advanced-electronics","sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistic-science-pack":{"unlocks":["logistic-science-pack"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"military-science-pack":{"unlocks":["military-science-pack"],"requires":["military-2","stone-wall"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"production-science-pack":{"unlocks":["production-science-pack"],"requires":["productivity-module","advanced-material-processing-2","railway"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"space-science-pack":{"unlocks":["satellite"],"requires":["rocket-silo","electric-energy-accumulators","solar-energy"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"steel-processing":{"unlocks":["steel-plate","steel-chest"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"utility-science-pack":{"unlocks":["utility-science-pack"],"requires":["robotics","advanced-electronics-2","low-density-structure"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"advanced-material-processing":{"unlocks":["steel-furnace"],"requires":["steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"steel-axe":{"unlocks":{},"requires":["steel-processing"],"ingredients":["automation-science-pack"],"has_modifier":true},"advanced-material-processing-2":{"unlocks":["electric-furnace"],"requires":["advanced-material-processing","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"concrete":{"unlocks":["concrete","hazard-concrete","refined-concrete","refined-hazard-concrete"],"requires":["advanced-material-processing","automation-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-accumulators":{"unlocks":["accumulator"],"requires":["electric-energy-distribution-1","battery"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-1":{"unlocks":["medium-electric-pole","big-electric-pole"],"requires":["electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-2":{"unlocks":["substation"],"requires":["electric-energy-distribution-1","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"railway":{"unlocks":["rail","locomotive","cargo-wagon"],"requires":["logistics-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fluid-wagon":{"unlocks":["fluid-wagon"],"requires":["railway","fluid-handling"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"automated-rail-transportation":{"unlocks":["train-stop"],"requires":["railway"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"rail-signals":{"unlocks":["rail-signal","rail-chain-signal"],"requires":["automated-rail-transportation"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"robotics":{"unlocks":["flying-robot-frame"],"requires":["electric-engine","battery"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"construction-robotics":{"unlocks":["roboport","logistic-chest-passive-provider","logistic-chest-storage","construction-robot"],"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"logistic-robotics":{"unlocks":["roboport","logistic-chest-passive-provider","logistic-chest-storage","logistic-robot"],"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"logistic-system":{"unlocks":["logistic-chest-active-provider","logistic-chest-requester","logistic-chest-buffer"],"requires":["utility-science-pack","logistic-robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"personal-roboport-equipment":{"unlocks":["personal-roboport-equipment"],"requires":["construction-robotics","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"personal-roboport-mk2-equipment":{"unlocks":["personal-roboport-mk2-equipment"],"requires":["personal-roboport-equipment","utility-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"worker-robots-speed-1":{"unlocks":{},"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-speed-2":{"unlocks":{},"requires":["worker-robots-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-speed-3":{"unlocks":{},"requires":["worker-robots-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-speed-4":{"unlocks":{},"requires":["worker-robots-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"mining-productivity-1":{"unlocks":{},"requires":["advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"mining-productivity-2":{"unlocks":{},"requires":["mining-productivity-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"mining-productivity-3":{"unlocks":{},"requires":["mining-productivity-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-speed-5":{"unlocks":{},"requires":["worker-robots-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-storage-1":{"unlocks":{},"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-storage-2":{"unlocks":{},"requires":["worker-robots-storage-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"worker-robots-storage-3":{"unlocks":{},"requires":["worker-robots-storage-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"toolbelt":{"unlocks":{},"requires":["logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-1":{"unlocks":{},"requires":["automation-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-2":{"unlocks":{},"requires":["research-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-3":{"unlocks":{},"requires":["research-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"research-speed-4":{"unlocks":{},"requires":["research-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"research-speed-5":{"unlocks":{},"requires":["research-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"research-speed-6":{"unlocks":{},"requires":["research-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"stack-inserter":{"unlocks":["stack-inserter","stack-filter-inserter"],"requires":["fast-inserter","logistics-2","advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-1":{"unlocks":{},"requires":["stack-inserter"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-2":{"unlocks":{},"requires":["inserter-capacity-bonus-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-3":{"unlocks":{},"requires":["inserter-capacity-bonus-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"inserter-capacity-bonus-4":{"unlocks":{},"requires":["inserter-capacity-bonus-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-5":{"unlocks":{},"requires":["inserter-capacity-bonus-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-6":{"unlocks":{},"requires":["inserter-capacity-bonus-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-7":{"unlocks":{},"requires":["inserter-capacity-bonus-6"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"oil-processing":{"unlocks":["pumpjack","oil-refinery","chemical-plant","basic-oil-processing","solid-fuel-from-petroleum-gas"],"requires":["fluid-handling"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fluid-handling":{"unlocks":["storage-tank","pump","empty-barrel","fill-water-barrel","empty-water-barrel","fill-sulfuric-acid-barrel","empty-sulfuric-acid-barrel","fill-crude-oil-barrel","empty-crude-oil-barrel","fill-heavy-oil-barrel","empty-heavy-oil-barrel","fill-light-oil-barrel","empty-light-oil-barrel","fill-petroleum-gas-barrel","empty-petroleum-gas-barrel","fill-lubricant-barrel","empty-lubricant-barrel"],"requires":["automation-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"advanced-oil-processing":{"unlocks":["advanced-oil-processing","heavy-oil-cracking","light-oil-cracking","solid-fuel-from-heavy-oil","solid-fuel-from-light-oil"],"requires":["chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"coal-liquefaction":{"unlocks":["coal-liquefaction"],"requires":["advanced-oil-processing","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"sulfur-processing":{"unlocks":["sulfuric-acid","sulfur"],"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"plastics":{"unlocks":["plastic-bar"],"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"artillery":{"unlocks":["artillery-wagon","artillery-turret","artillery-shell","artillery-targeting-remote"],"requires":["military-4","tank"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"spidertron":{"unlocks":["spidertron","spidertron-remote"],"requires":["military-4","exoskeleton-equipment","fusion-reactor-equipment","rocketry","rocket-control-unit","effectivity-module-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"military":{"unlocks":["submachine-gun","shotgun","shotgun-shell"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"atomic-bomb":{"unlocks":["atomic-bomb"],"requires":["military-4","kovarex-enrichment-process","rocket-control-unit","rocketry"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"military-2":{"unlocks":["piercing-rounds-magazine","grenade"],"requires":["military","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"uranium-ammo":{"unlocks":["uranium-rounds-magazine","uranium-cannon-shell","explosive-uranium-cannon-shell"],"requires":["uranium-processing","military-4","tank"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"military-3":{"unlocks":["poison-capsule","slowdown-capsule","combat-shotgun"],"requires":["chemical-science-pack","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"military-4":{"unlocks":["piercing-shotgun-shell","cluster-grenade"],"requires":["military-3","utility-science-pack","explosives"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"automobilism":{"unlocks":["car"],"requires":["logistics-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"flammables":{"unlocks":{},"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"flamethrower":{"unlocks":["flamethrower","flamethrower-ammo","flamethrower-turret"],"requires":["flammables","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"tank":{"unlocks":["tank","cannon-shell","explosive-cannon-shell"],"requires":["automobilism","military-3","explosives"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"land-mine":{"unlocks":["land-mine"],"requires":["explosives","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"rocketry":{"unlocks":["rocket-launcher","rocket"],"requires":["explosives","flammables","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"explosive-rocketry":{"unlocks":["explosive-rocket"],"requires":["rocketry","military-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"energy-weapons-damage-1":{"unlocks":{},"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"refined-flammables-1":{"unlocks":{},"requires":["flamethrower"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"stronger-explosives-1":{"unlocks":{},"requires":["military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"weapon-shooting-speed-1":{"unlocks":{},"requires":["military"],"ingredients":["automation-science-pack"],"has_modifier":true},"physical-projectile-damage-1":{"unlocks":{},"requires":["military"],"ingredients":["automation-science-pack"],"has_modifier":true},"energy-weapons-damage-2":{"unlocks":{},"requires":["energy-weapons-damage-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-2":{"unlocks":{},"requires":["physical-projectile-damage-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"refined-flammables-2":{"unlocks":{},"requires":["refined-flammables-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"stronger-explosives-2":{"unlocks":{},"requires":["stronger-explosives-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"weapon-shooting-speed-2":{"unlocks":{},"requires":["weapon-shooting-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"energy-weapons-damage-3":{"unlocks":{},"requires":["energy-weapons-damage-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-3":{"unlocks":{},"requires":["physical-projectile-damage-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"refined-flammables-3":{"unlocks":{},"requires":["refined-flammables-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"stronger-explosives-3":{"unlocks":{},"requires":["stronger-explosives-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"weapon-shooting-speed-3":{"unlocks":{},"requires":["weapon-shooting-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"energy-weapons-damage-4":{"unlocks":{},"requires":["energy-weapons-damage-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-4":{"unlocks":{},"requires":["physical-projectile-damage-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"refined-flammables-4":{"unlocks":{},"requires":["refined-flammables-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-4":{"unlocks":{},"requires":["stronger-explosives-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-4":{"unlocks":{},"requires":["weapon-shooting-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"energy-weapons-damage-5":{"unlocks":{},"requires":["energy-weapons-damage-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"physical-projectile-damage-5":{"unlocks":{},"requires":["physical-projectile-damage-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"refined-flammables-5":{"unlocks":{},"requires":["refined-flammables-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-5":{"unlocks":{},"requires":["stronger-explosives-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-5":{"unlocks":{},"requires":["weapon-shooting-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"energy-weapons-damage-6":{"unlocks":{},"requires":["energy-weapons-damage-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"physical-projectile-damage-6":{"unlocks":{},"requires":["physical-projectile-damage-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"refined-flammables-6":{"unlocks":{},"requires":["refined-flammables-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-6":{"unlocks":{},"requires":["stronger-explosives-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-6":{"unlocks":{},"requires":["weapon-shooting-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-1":{"unlocks":{},"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-2":{"unlocks":{},"requires":["laser-shooting-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-3":{"unlocks":{},"requires":["laser-shooting-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-4":{"unlocks":{},"requires":["laser-shooting-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-5":{"unlocks":{},"requires":["laser-shooting-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-6":{"unlocks":{},"requires":["laser-shooting-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-7":{"unlocks":{},"requires":["laser-shooting-speed-6"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"defender":{"unlocks":["defender-capsule"],"requires":["military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"distractor":{"unlocks":["distractor-capsule"],"requires":["defender","military-3","laser"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"destroyer":{"unlocks":["destroyer-capsule"],"requires":["military-4","distractor","speed-module"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"follower-robot-count-1":{"unlocks":{},"requires":["defender"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"follower-robot-count-2":{"unlocks":{},"requires":["follower-robot-count-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"follower-robot-count-3":{"unlocks":{},"requires":["follower-robot-count-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"follower-robot-count-4":{"unlocks":{},"requires":["follower-robot-count-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"follower-robot-count-5":{"unlocks":{},"requires":["follower-robot-count-4","destroyer"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"follower-robot-count-6":{"unlocks":{},"requires":["follower-robot-count-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"kovarex-enrichment-process":{"unlocks":["kovarex-enrichment-process","nuclear-fuel"],"requires":["production-science-pack","uranium-processing","rocket-fuel"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"nuclear-fuel-reprocessing":{"unlocks":["nuclear-fuel-reprocessing"],"requires":["nuclear-power","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"nuclear-power":{"unlocks":["nuclear-reactor","heat-exchanger","heat-pipe","steam-turbine"],"requires":["uranium-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"uranium-processing":{"unlocks":["centrifuge","uranium-processing","uranium-fuel-cell"],"requires":["chemical-science-pack","concrete"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"heavy-armor":{"unlocks":["heavy-armor"],"requires":["military","steel-processing"],"ingredients":["automation-science-pack"],"has_modifier":false},"modular-armor":{"unlocks":["modular-armor"],"requires":["heavy-armor","advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"power-armor":{"unlocks":["power-armor"],"requires":["modular-armor","electric-engine","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"power-armor-mk2":{"unlocks":["power-armor-mk2"],"requires":["power-armor","military-4","speed-module-2","effectivity-module-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"energy-shield-equipment":{"unlocks":["energy-shield-equipment"],"requires":["solar-panel-equipment","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"energy-shield-mk2-equipment":{"unlocks":["energy-shield-mk2-equipment"],"requires":["energy-shield-equipment","military-3","low-density-structure","power-armor"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"night-vision-equipment":{"unlocks":["night-vision-equipment"],"requires":["solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"belt-immunity-equipment":{"unlocks":["belt-immunity-equipment"],"requires":["solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"exoskeleton-equipment":{"unlocks":["exoskeleton-equipment"],"requires":["advanced-electronics-2","electric-engine","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"battery-equipment":{"unlocks":["battery-equipment"],"requires":["battery","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"battery-mk2-equipment":{"unlocks":["battery-mk2-equipment"],"requires":["battery-equipment","low-density-structure","power-armor"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"solar-panel-equipment":{"unlocks":["solar-panel-equipment"],"requires":["modular-armor","solar-energy"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fusion-reactor-equipment":{"unlocks":["fusion-reactor-equipment"],"requires":["utility-science-pack","power-armor","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"personal-laser-defense-equipment":{"unlocks":["personal-laser-defense-equipment"],"requires":["laser-turret","military-3","low-density-structure","power-armor","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"discharge-defense-equipment":{"unlocks":["discharge-defense-equipment","discharge-defense-remote"],"requires":["laser-turret","military-3","power-armor","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"modules":{"unlocks":{},"requires":["advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"speed-module":{"unlocks":["speed-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"speed-module-2":{"unlocks":["speed-module-2"],"requires":["speed-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"speed-module-3":{"unlocks":["speed-module-3"],"requires":["speed-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"productivity-module":{"unlocks":["productivity-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"productivity-module-2":{"unlocks":["productivity-module-2"],"requires":["productivity-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"productivity-module-3":{"unlocks":["productivity-module-3"],"requires":["productivity-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"effectivity-module":{"unlocks":["effectivity-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"effectivity-module-2":{"unlocks":["effectivity-module-2"],"requires":["effectivity-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"effectivity-module-3":{"unlocks":["effectivity-module-3"],"requires":["effectivity-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"effect-transmission":{"unlocks":["beacon"],"requires":["advanced-electronics-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"low-density-structure":{"unlocks":["low-density-structure"],"requires":["advanced-material-processing","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"rocket-control-unit":{"unlocks":["rocket-control-unit"],"requires":["utility-science-pack","speed-module"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"rocket-fuel":{"unlocks":["rocket-fuel"],"requires":["flammables","advanced-oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"rocket-silo":{"unlocks":["rocket-silo","rocket-part"],"requires":["concrete","speed-module-3","productivity-module-3","rocket-fuel","rocket-control-unit"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"cliff-explosives":{"unlocks":["cliff-explosives"],"requires":["explosives","military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false}} \ No newline at end of file +{"advanced-circuit":{"unlocks":["advanced-circuit"],"requires":["plastics"],"has_modifier":false},"advanced-combinators":{"unlocks":["selector-combinator"],"requires":["circuit-network","chemical-science-pack"],"has_modifier":false},"advanced-material-processing":{"unlocks":["steel-furnace"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"advanced-material-processing-2":{"unlocks":["electric-furnace"],"requires":["advanced-material-processing","chemical-science-pack"],"has_modifier":false},"advanced-oil-processing":{"unlocks":["advanced-oil-processing","heavy-oil-cracking","light-oil-cracking","solid-fuel-from-heavy-oil","solid-fuel-from-light-oil"],"requires":["chemical-science-pack"],"has_modifier":false},"artillery":{"unlocks":["artillery-wagon","artillery-turret","artillery-shell"],"requires":["military-4","tank","concrete","radar"],"has_modifier":false},"atomic-bomb":{"unlocks":["atomic-bomb"],"requires":["military-4","kovarex-enrichment-process","rocketry"],"has_modifier":false},"automated-rail-transportation":{"unlocks":["train-stop","rail-signal","rail-chain-signal"],"requires":["railway"],"has_modifier":false},"automation":{"unlocks":["assembling-machine-1","long-handed-inserter"],"requires":["automation-science-pack"],"has_modifier":false},"automation-2":{"unlocks":["assembling-machine-2"],"requires":["automation","steel-processing","logistic-science-pack"],"has_modifier":false},"automation-3":{"unlocks":["assembling-machine-3"],"requires":["speed-module","production-science-pack","electric-engine"],"has_modifier":false},"automation-science-pack":{"unlocks":["automation-science-pack"],"requires":["steam-power","electronics"],"has_modifier":false},"automobilism":{"unlocks":["car"],"requires":["logistics-2","engine"],"has_modifier":false},"battery":{"unlocks":["battery"],"requires":["sulfur-processing"],"has_modifier":false},"battery-equipment":{"unlocks":["battery-equipment"],"requires":["battery","solar-panel-equipment"],"has_modifier":false},"battery-mk2-equipment":{"unlocks":["battery-mk2-equipment"],"requires":["battery-equipment","low-density-structure","power-armor"],"has_modifier":false},"belt-immunity-equipment":{"unlocks":["belt-immunity-equipment"],"requires":["solar-panel-equipment"],"has_modifier":false},"braking-force-1":{"unlocks":{},"requires":["railway","chemical-science-pack"],"has_modifier":true},"braking-force-2":{"unlocks":{},"requires":["braking-force-1"],"has_modifier":true},"braking-force-3":{"unlocks":{},"requires":["braking-force-2","production-science-pack"],"has_modifier":true},"braking-force-4":{"unlocks":{},"requires":["braking-force-3"],"has_modifier":true},"braking-force-5":{"unlocks":{},"requires":["braking-force-4"],"has_modifier":true},"braking-force-6":{"unlocks":{},"requires":["braking-force-5","utility-science-pack"],"has_modifier":true},"braking-force-7":{"unlocks":{},"requires":["braking-force-6"],"has_modifier":true},"bulk-inserter":{"unlocks":["bulk-inserter"],"requires":["fast-inserter","logistics-2","advanced-circuit"],"has_modifier":true},"chemical-science-pack":{"unlocks":["chemical-science-pack"],"requires":["advanced-circuit","sulfur-processing"],"has_modifier":false},"circuit-network":{"unlocks":["arithmetic-combinator","decider-combinator","constant-combinator","power-switch","programmable-speaker","display-panel","iron-stick"],"requires":["logistic-science-pack"],"has_modifier":true},"cliff-explosives":{"unlocks":["cliff-explosives"],"requires":["explosives","military-2"],"has_modifier":true},"coal-liquefaction":{"unlocks":["coal-liquefaction"],"requires":["advanced-oil-processing","production-science-pack"],"has_modifier":false},"concrete":{"unlocks":["concrete","hazard-concrete","refined-concrete","refined-hazard-concrete","iron-stick"],"requires":["advanced-material-processing","automation-2"],"has_modifier":false},"construction-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","construction-robot"],"requires":["robotics"],"has_modifier":true},"defender":{"unlocks":["defender-capsule"],"requires":["military-science-pack"],"has_modifier":true},"destroyer":{"unlocks":["destroyer-capsule"],"requires":["military-4","distractor","speed-module"],"has_modifier":false},"discharge-defense-equipment":{"unlocks":["discharge-defense-equipment"],"requires":["laser-turret","military-3","power-armor","solar-panel-equipment"],"has_modifier":false},"distractor":{"unlocks":["distractor-capsule"],"requires":["defender","military-3","laser"],"has_modifier":false},"effect-transmission":{"unlocks":["beacon"],"requires":["processing-unit","production-science-pack"],"has_modifier":false},"efficiency-module":{"unlocks":["efficiency-module"],"requires":["modules"],"has_modifier":false},"efficiency-module-2":{"unlocks":["efficiency-module-2"],"requires":["efficiency-module","processing-unit"],"has_modifier":false},"efficiency-module-3":{"unlocks":["efficiency-module-3"],"requires":["efficiency-module-2","production-science-pack"],"has_modifier":false},"electric-energy-accumulators":{"unlocks":["accumulator"],"requires":["electric-energy-distribution-1","battery"],"has_modifier":false},"electric-energy-distribution-1":{"unlocks":["medium-electric-pole","big-electric-pole","iron-stick"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-2":{"unlocks":["substation"],"requires":["electric-energy-distribution-1","chemical-science-pack"],"has_modifier":false},"electric-engine":{"unlocks":["electric-engine-unit"],"requires":["lubricant"],"has_modifier":false},"electric-mining-drill":{"unlocks":["electric-mining-drill"],"requires":["automation-science-pack"],"has_modifier":false},"electronics":{"unlocks":["copper-cable","electronic-circuit","lab","inserter","small-electric-pole"],"requires":{},"has_modifier":false},"energy-shield-equipment":{"unlocks":["energy-shield-equipment"],"requires":["solar-panel-equipment","military-science-pack"],"has_modifier":false},"energy-shield-mk2-equipment":{"unlocks":["energy-shield-mk2-equipment"],"requires":["energy-shield-equipment","military-3","low-density-structure","power-armor"],"has_modifier":false},"engine":{"unlocks":["engine-unit"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"exoskeleton-equipment":{"unlocks":["exoskeleton-equipment"],"requires":["processing-unit","electric-engine","solar-panel-equipment"],"has_modifier":false},"explosive-rocketry":{"unlocks":["explosive-rocket"],"requires":["rocketry","military-3"],"has_modifier":false},"explosives":{"unlocks":["explosives"],"requires":["sulfur-processing"],"has_modifier":false},"fast-inserter":{"unlocks":["fast-inserter"],"requires":["automation-science-pack"],"has_modifier":false},"fission-reactor-equipment":{"unlocks":["fission-reactor-equipment"],"requires":["utility-science-pack","power-armor","military-science-pack","nuclear-power"],"has_modifier":false},"flamethrower":{"unlocks":["flamethrower","flamethrower-ammo","flamethrower-turret"],"requires":["flammables","military-science-pack"],"has_modifier":false},"flammables":{"unlocks":{},"requires":["oil-processing"],"has_modifier":false},"fluid-handling":{"unlocks":["storage-tank","pump","barrel","water-barrel","empty-water-barrel","sulfuric-acid-barrel","empty-sulfuric-acid-barrel","crude-oil-barrel","empty-crude-oil-barrel","heavy-oil-barrel","empty-heavy-oil-barrel","light-oil-barrel","empty-light-oil-barrel","petroleum-gas-barrel","empty-petroleum-gas-barrel","lubricant-barrel","empty-lubricant-barrel"],"requires":["automation-2","engine"],"has_modifier":false},"fluid-wagon":{"unlocks":["fluid-wagon"],"requires":["railway","fluid-handling"],"has_modifier":false},"follower-robot-count-1":{"unlocks":{},"requires":["defender"],"has_modifier":true},"follower-robot-count-2":{"unlocks":{},"requires":["follower-robot-count-1"],"has_modifier":true},"follower-robot-count-3":{"unlocks":{},"requires":["follower-robot-count-2","chemical-science-pack"],"has_modifier":true},"follower-robot-count-4":{"unlocks":{},"requires":["follower-robot-count-3","destroyer"],"has_modifier":true},"gate":{"unlocks":["gate"],"requires":["stone-wall","military-2"],"has_modifier":false},"gun-turret":{"unlocks":["gun-turret"],"requires":["automation-science-pack"],"has_modifier":false},"heavy-armor":{"unlocks":["heavy-armor"],"requires":["military","steel-processing"],"has_modifier":false},"inserter-capacity-bonus-1":{"unlocks":{},"requires":["bulk-inserter"],"has_modifier":true},"inserter-capacity-bonus-2":{"unlocks":{},"requires":["inserter-capacity-bonus-1"],"has_modifier":true},"inserter-capacity-bonus-3":{"unlocks":{},"requires":["inserter-capacity-bonus-2","chemical-science-pack"],"has_modifier":true},"inserter-capacity-bonus-4":{"unlocks":{},"requires":["inserter-capacity-bonus-3","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-5":{"unlocks":{},"requires":["inserter-capacity-bonus-4"],"has_modifier":true},"inserter-capacity-bonus-6":{"unlocks":{},"requires":["inserter-capacity-bonus-5"],"has_modifier":true},"inserter-capacity-bonus-7":{"unlocks":{},"requires":["inserter-capacity-bonus-6","utility-science-pack"],"has_modifier":true},"kovarex-enrichment-process":{"unlocks":["kovarex-enrichment-process","nuclear-fuel"],"requires":["production-science-pack","uranium-processing","rocket-fuel"],"has_modifier":false},"lamp":{"unlocks":["small-lamp"],"requires":["automation-science-pack"],"has_modifier":false},"land-mine":{"unlocks":["land-mine"],"requires":["explosives","military-science-pack"],"has_modifier":false},"landfill":{"unlocks":["landfill"],"requires":["logistic-science-pack"],"has_modifier":false},"laser":{"unlocks":{},"requires":["battery","chemical-science-pack"],"has_modifier":false},"laser-shooting-speed-1":{"unlocks":{},"requires":["laser","military-science-pack"],"has_modifier":true},"laser-shooting-speed-2":{"unlocks":{},"requires":["laser-shooting-speed-1"],"has_modifier":true},"laser-shooting-speed-3":{"unlocks":{},"requires":["laser-shooting-speed-2"],"has_modifier":true},"laser-shooting-speed-4":{"unlocks":{},"requires":["laser-shooting-speed-3"],"has_modifier":true},"laser-shooting-speed-5":{"unlocks":{},"requires":["laser-shooting-speed-4","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-6":{"unlocks":{},"requires":["laser-shooting-speed-5"],"has_modifier":true},"laser-shooting-speed-7":{"unlocks":{},"requires":["laser-shooting-speed-6"],"has_modifier":true},"laser-turret":{"unlocks":["laser-turret"],"requires":["laser","military-science-pack"],"has_modifier":false},"laser-weapons-damage-1":{"unlocks":{},"requires":["laser","military-science-pack"],"has_modifier":true},"laser-weapons-damage-2":{"unlocks":{},"requires":["laser-weapons-damage-1"],"has_modifier":true},"laser-weapons-damage-3":{"unlocks":{},"requires":["laser-weapons-damage-2"],"has_modifier":true},"laser-weapons-damage-4":{"unlocks":{},"requires":["laser-weapons-damage-3"],"has_modifier":true},"laser-weapons-damage-5":{"unlocks":{},"requires":["laser-weapons-damage-4","utility-science-pack"],"has_modifier":true},"laser-weapons-damage-6":{"unlocks":{},"requires":["laser-weapons-damage-5"],"has_modifier":true},"logistic-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","logistic-robot"],"requires":["robotics"],"has_modifier":true},"logistic-science-pack":{"unlocks":["logistic-science-pack"],"requires":["automation-science-pack"],"has_modifier":false},"logistic-system":{"unlocks":["active-provider-chest","requester-chest","buffer-chest"],"requires":["utility-science-pack","logistic-robotics"],"has_modifier":true},"logistics":{"unlocks":["underground-belt","splitter"],"requires":["automation-science-pack"],"has_modifier":false},"logistics-2":{"unlocks":["fast-transport-belt","fast-underground-belt","fast-splitter"],"requires":["logistics","logistic-science-pack"],"has_modifier":false},"logistics-3":{"unlocks":["express-transport-belt","express-underground-belt","express-splitter"],"requires":["production-science-pack","lubricant"],"has_modifier":false},"low-density-structure":{"unlocks":["low-density-structure"],"requires":["advanced-material-processing","chemical-science-pack"],"has_modifier":false},"lubricant":{"unlocks":["lubricant"],"requires":["advanced-oil-processing"],"has_modifier":false},"military":{"unlocks":["submachine-gun","shotgun","shotgun-shell"],"requires":["automation-science-pack"],"has_modifier":false},"military-2":{"unlocks":["piercing-rounds-magazine","grenade"],"requires":["military","steel-processing","logistic-science-pack"],"has_modifier":false},"military-3":{"unlocks":["poison-capsule","slowdown-capsule","combat-shotgun"],"requires":["chemical-science-pack","military-science-pack"],"has_modifier":false},"military-4":{"unlocks":["piercing-shotgun-shell","cluster-grenade"],"requires":["military-3","utility-science-pack","explosives"],"has_modifier":false},"military-science-pack":{"unlocks":["military-science-pack"],"requires":["military-2","stone-wall"],"has_modifier":false},"mining-productivity-1":{"unlocks":{},"requires":["advanced-circuit"],"has_modifier":true},"mining-productivity-2":{"unlocks":{},"requires":["mining-productivity-1","chemical-science-pack"],"has_modifier":true},"mining-productivity-3":{"unlocks":{},"requires":["mining-productivity-2","production-science-pack","utility-science-pack"],"has_modifier":true},"modular-armor":{"unlocks":["modular-armor"],"requires":["heavy-armor","advanced-circuit"],"has_modifier":false},"modules":{"unlocks":{},"requires":["advanced-circuit"],"has_modifier":false},"night-vision-equipment":{"unlocks":["night-vision-equipment"],"requires":["solar-panel-equipment"],"has_modifier":false},"nuclear-fuel-reprocessing":{"unlocks":["nuclear-fuel-reprocessing"],"requires":["nuclear-power","production-science-pack"],"has_modifier":false},"nuclear-power":{"unlocks":["nuclear-reactor","heat-exchanger","heat-pipe","steam-turbine","uranium-fuel-cell"],"requires":["uranium-processing"],"has_modifier":false},"oil-gathering":{"unlocks":["pumpjack"],"requires":["fluid-handling"],"has_modifier":false},"oil-processing":{"unlocks":["oil-refinery","chemical-plant","basic-oil-processing","solid-fuel-from-petroleum-gas"],"requires":["oil-gathering"],"has_modifier":false},"personal-laser-defense-equipment":{"unlocks":["personal-laser-defense-equipment"],"requires":["laser-turret","military-3","low-density-structure","power-armor","solar-panel-equipment"],"has_modifier":false},"personal-roboport-equipment":{"unlocks":["personal-roboport-equipment"],"requires":["construction-robotics","solar-panel-equipment"],"has_modifier":false},"personal-roboport-mk2-equipment":{"unlocks":["personal-roboport-mk2-equipment"],"requires":["personal-roboport-equipment","utility-science-pack"],"has_modifier":false},"physical-projectile-damage-1":{"unlocks":{},"requires":["military"],"has_modifier":true},"physical-projectile-damage-2":{"unlocks":{},"requires":["physical-projectile-damage-1","logistic-science-pack"],"has_modifier":true},"physical-projectile-damage-3":{"unlocks":{},"requires":["physical-projectile-damage-2","military-science-pack"],"has_modifier":true},"physical-projectile-damage-4":{"unlocks":{},"requires":["physical-projectile-damage-3"],"has_modifier":true},"physical-projectile-damage-5":{"unlocks":{},"requires":["physical-projectile-damage-4","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-6":{"unlocks":{},"requires":["physical-projectile-damage-5","utility-science-pack"],"has_modifier":true},"plastics":{"unlocks":["plastic-bar"],"requires":["oil-processing"],"has_modifier":false},"power-armor":{"unlocks":["power-armor"],"requires":["modular-armor","electric-engine","processing-unit"],"has_modifier":false},"power-armor-mk2":{"unlocks":["power-armor-mk2"],"requires":["power-armor","military-4","speed-module-2","efficiency-module-2"],"has_modifier":false},"processing-unit":{"unlocks":["processing-unit"],"requires":["chemical-science-pack"],"has_modifier":false},"production-science-pack":{"unlocks":["production-science-pack"],"requires":["productivity-module","advanced-material-processing-2","railway"],"has_modifier":false},"productivity-module":{"unlocks":["productivity-module"],"requires":["modules"],"has_modifier":false},"productivity-module-2":{"unlocks":["productivity-module-2"],"requires":["productivity-module","processing-unit"],"has_modifier":false},"productivity-module-3":{"unlocks":["productivity-module-3"],"requires":["productivity-module-2","production-science-pack"],"has_modifier":false},"radar":{"unlocks":["radar"],"requires":["automation-science-pack"],"has_modifier":false},"railway":{"unlocks":["rail","locomotive","cargo-wagon","iron-stick"],"requires":["logistics-2","engine"],"has_modifier":false},"refined-flammables-1":{"unlocks":{},"requires":["flamethrower"],"has_modifier":true},"refined-flammables-2":{"unlocks":{},"requires":["refined-flammables-1"],"has_modifier":true},"refined-flammables-3":{"unlocks":{},"requires":["refined-flammables-2","chemical-science-pack"],"has_modifier":true},"refined-flammables-4":{"unlocks":{},"requires":["refined-flammables-3","utility-science-pack"],"has_modifier":true},"refined-flammables-5":{"unlocks":{},"requires":["refined-flammables-4"],"has_modifier":true},"refined-flammables-6":{"unlocks":{},"requires":["refined-flammables-5"],"has_modifier":true},"repair-pack":{"unlocks":["repair-pack"],"requires":["automation-science-pack"],"has_modifier":false},"research-speed-1":{"unlocks":{},"requires":["automation-2"],"has_modifier":true},"research-speed-2":{"unlocks":{},"requires":["research-speed-1"],"has_modifier":true},"research-speed-3":{"unlocks":{},"requires":["research-speed-2","chemical-science-pack"],"has_modifier":true},"research-speed-4":{"unlocks":{},"requires":["research-speed-3"],"has_modifier":true},"research-speed-5":{"unlocks":{},"requires":["research-speed-4","production-science-pack"],"has_modifier":true},"research-speed-6":{"unlocks":{},"requires":["research-speed-5","utility-science-pack"],"has_modifier":true},"robotics":{"unlocks":["flying-robot-frame"],"requires":["electric-engine","battery"],"has_modifier":false},"rocket-fuel":{"unlocks":["rocket-fuel"],"requires":["flammables","advanced-oil-processing"],"has_modifier":false},"rocket-silo":{"unlocks":["rocket-silo","rocket-part","cargo-landing-pad","satellite"],"requires":["concrete","rocket-fuel","electric-energy-accumulators","solar-energy","utility-science-pack","speed-module-3","productivity-module-3","radar"],"has_modifier":false},"rocketry":{"unlocks":["rocket-launcher","rocket"],"requires":["explosives","flammables","military-science-pack"],"has_modifier":false},"solar-energy":{"unlocks":["solar-panel"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"solar-panel-equipment":{"unlocks":["solar-panel-equipment"],"requires":["modular-armor","solar-energy"],"has_modifier":false},"space-science-pack":{"unlocks":{},"requires":["rocket-silo"],"has_modifier":false},"speed-module":{"unlocks":["speed-module"],"requires":["modules"],"has_modifier":false},"speed-module-2":{"unlocks":["speed-module-2"],"requires":["speed-module","processing-unit"],"has_modifier":false},"speed-module-3":{"unlocks":["speed-module-3"],"requires":["speed-module-2","production-science-pack"],"has_modifier":false},"spidertron":{"unlocks":["spidertron"],"requires":["military-4","exoskeleton-equipment","fission-reactor-equipment","rocketry","efficiency-module-3","radar"],"has_modifier":false},"steam-power":{"unlocks":["pipe","pipe-to-ground","offshore-pump","boiler","steam-engine"],"requires":{},"has_modifier":false},"steel-axe":{"unlocks":{},"requires":["steel-processing"],"has_modifier":true},"steel-processing":{"unlocks":["steel-plate","steel-chest"],"requires":["automation-science-pack"],"has_modifier":false},"stone-wall":{"unlocks":["stone-wall"],"requires":["automation-science-pack"],"has_modifier":false},"stronger-explosives-1":{"unlocks":{},"requires":["military-2"],"has_modifier":true},"stronger-explosives-2":{"unlocks":{},"requires":["stronger-explosives-1","military-science-pack"],"has_modifier":true},"stronger-explosives-3":{"unlocks":{},"requires":["stronger-explosives-2","chemical-science-pack"],"has_modifier":true},"stronger-explosives-4":{"unlocks":{},"requires":["stronger-explosives-3","utility-science-pack"],"has_modifier":true},"stronger-explosives-5":{"unlocks":{},"requires":["stronger-explosives-4"],"has_modifier":true},"stronger-explosives-6":{"unlocks":{},"requires":["stronger-explosives-5"],"has_modifier":true},"sulfur-processing":{"unlocks":["sulfuric-acid","sulfur"],"requires":["oil-processing"],"has_modifier":false},"tank":{"unlocks":["tank","cannon-shell","explosive-cannon-shell"],"requires":["automobilism","military-3","explosives"],"has_modifier":false},"toolbelt":{"unlocks":{},"requires":["logistic-science-pack"],"has_modifier":true},"uranium-ammo":{"unlocks":["uranium-rounds-magazine","uranium-cannon-shell","explosive-uranium-cannon-shell"],"requires":["uranium-processing","military-4","tank"],"has_modifier":false},"uranium-mining":{"unlocks":{},"requires":["chemical-science-pack","concrete"],"has_modifier":true},"uranium-processing":{"unlocks":["centrifuge","uranium-processing"],"requires":["uranium-mining"],"has_modifier":false},"utility-science-pack":{"unlocks":["utility-science-pack"],"requires":["robotics","processing-unit","low-density-structure"],"has_modifier":false},"weapon-shooting-speed-1":{"unlocks":{},"requires":["military"],"has_modifier":true},"weapon-shooting-speed-2":{"unlocks":{},"requires":["weapon-shooting-speed-1","logistic-science-pack"],"has_modifier":true},"weapon-shooting-speed-3":{"unlocks":{},"requires":["weapon-shooting-speed-2","military-science-pack"],"has_modifier":true},"weapon-shooting-speed-4":{"unlocks":{},"requires":["weapon-shooting-speed-3"],"has_modifier":true},"weapon-shooting-speed-5":{"unlocks":{},"requires":["weapon-shooting-speed-4","chemical-science-pack"],"has_modifier":true},"weapon-shooting-speed-6":{"unlocks":{},"requires":["weapon-shooting-speed-5","utility-science-pack"],"has_modifier":true},"worker-robots-speed-1":{"unlocks":{},"requires":["robotics"],"has_modifier":true},"worker-robots-speed-2":{"unlocks":{},"requires":["worker-robots-speed-1"],"has_modifier":true},"worker-robots-speed-3":{"unlocks":{},"requires":["worker-robots-speed-2","utility-science-pack"],"has_modifier":true},"worker-robots-speed-4":{"unlocks":{},"requires":["worker-robots-speed-3"],"has_modifier":true},"worker-robots-speed-5":{"unlocks":{},"requires":["worker-robots-speed-4","production-science-pack"],"has_modifier":true},"worker-robots-storage-1":{"unlocks":{},"requires":["robotics"],"has_modifier":true},"worker-robots-storage-2":{"unlocks":{},"requires":["worker-robots-storage-1","production-science-pack"],"has_modifier":true},"worker-robots-storage-3":{"unlocks":{},"requires":["worker-robots-storage-2","utility-science-pack"],"has_modifier":true}} \ No newline at end of file diff --git a/worlds/faxanadu/Items.py b/worlds/faxanadu/Items.py new file mode 100644 index 000000000000..4815fde9de66 --- /dev/null +++ b/worlds/faxanadu/Items.py @@ -0,0 +1,58 @@ +from BaseClasses import ItemClassification +from typing import List, Optional + + +class ItemDef: + def __init__(self, + id: Optional[int], + name: str, + classification: ItemClassification, + count: int, + progression_count: int, + prefill_location: Optional[str]): + self.id = id + self.name = name + self.classification = classification + self.count = count + self.progression_count = progression_count + self.prefill_location = prefill_location + + +items: List[ItemDef] = [ + ItemDef(400000, 'Progressive Sword', ItemClassification.progression, 4, 0, None), + ItemDef(400001, 'Progressive Armor', ItemClassification.progression, 3, 0, None), + ItemDef(400002, 'Progressive Shield', ItemClassification.useful, 4, 0, None), + ItemDef(400003, 'Spring Elixir', ItemClassification.progression, 1, 0, None), + ItemDef(400004, 'Mattock', ItemClassification.progression, 1, 0, None), + ItemDef(400005, 'Unlock Wingboots', ItemClassification.progression, 1, 0, None), + ItemDef(400006, 'Key Jack', ItemClassification.progression, 1, 0, None), + ItemDef(400007, 'Key Queen', ItemClassification.progression, 1, 0, None), + ItemDef(400008, 'Key King', ItemClassification.progression, 1, 0, None), + ItemDef(400009, 'Key Joker', ItemClassification.progression, 1, 0, None), + ItemDef(400010, 'Key Ace', ItemClassification.progression, 1, 0, None), + ItemDef(400011, 'Ring of Ruby', ItemClassification.progression, 1, 0, None), + ItemDef(400012, 'Ring of Dworf', ItemClassification.progression, 1, 0, None), + ItemDef(400013, 'Demons Ring', ItemClassification.progression, 1, 0, None), + ItemDef(400014, 'Black Onyx', ItemClassification.progression, 1, 0, None), + ItemDef(None, 'Sky Spring Flow', ItemClassification.progression, 1, 0, 'Sky Spring'), + ItemDef(None, 'Tower of Fortress Spring Flow', ItemClassification.progression, 1, 0, 'Tower of Fortress Spring'), + ItemDef(None, 'Joker Spring Flow', ItemClassification.progression, 1, 0, 'Joker Spring'), + ItemDef(400015, 'Deluge', ItemClassification.progression, 1, 0, None), + ItemDef(400016, 'Thunder', ItemClassification.useful, 1, 0, None), + ItemDef(400017, 'Fire', ItemClassification.useful, 1, 0, None), + ItemDef(400018, 'Death', ItemClassification.useful, 1, 0, None), + ItemDef(400019, 'Tilte', ItemClassification.useful, 1, 0, None), + ItemDef(400020, 'Ring of Elf', ItemClassification.useful, 1, 0, None), + ItemDef(400021, 'Magical Rod', ItemClassification.useful, 1, 0, None), + ItemDef(400022, 'Pendant', ItemClassification.useful, 1, 0, None), + ItemDef(400023, 'Hourglass', ItemClassification.filler, 6, 0, None), + # We need at least 4 red potions for the Tower of Red Potion. Up to the player to save them up! + ItemDef(400024, 'Red Potion', ItemClassification.filler, 15, 4, None), + ItemDef(400025, 'Elixir', ItemClassification.filler, 4, 0, None), + ItemDef(400026, 'Glove', ItemClassification.filler, 5, 0, None), + ItemDef(400027, 'Ointment', ItemClassification.filler, 8, 0, None), + ItemDef(400028, 'Poison', ItemClassification.trap, 13, 0, None), + ItemDef(None, 'Killed Evil One', ItemClassification.progression, 1, 0, 'Evil One'), + # Placeholder item so the game knows which shop slot to prefill wingboots + ItemDef(400029, 'Wingboots', ItemClassification.useful, 0, 0, None), +] diff --git a/worlds/faxanadu/Locations.py b/worlds/faxanadu/Locations.py new file mode 100644 index 000000000000..ebb785f9391a --- /dev/null +++ b/worlds/faxanadu/Locations.py @@ -0,0 +1,199 @@ +from typing import List, Optional + + +class LocationType(): + world = 1 # Just standing there in the world + hidden = 2 # Kill all monsters in the room to reveal, each "item room" counter tick. + boss_reward = 3 # Kill a boss to reveal the item + shop = 4 # Buy at a shop + give = 5 # Given by an NPC + spring = 6 # Activatable spring + boss = 7 # Entity to kill to trigger the check + + +class ItemType(): + unknown = 0 # Or don't care + red_potion = 1 + + +class LocationDef: + def __init__(self, id: Optional[int], name: str, region: str, type: int, original_item: int): + self.id = id + self.name = name + self.region = region + self.type = type + self.original_item = original_item + + +locations: List[LocationDef] = [ + # Eolis + LocationDef(400100, 'Eolis Guru', 'Eolis', LocationType.give, ItemType.unknown), + LocationDef(400101, 'Eolis Key Jack', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400102, 'Eolis Hand Dagger', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400103, 'Eolis Red Potion', 'Eolis', LocationType.shop, ItemType.red_potion), + LocationDef(400104, 'Eolis Elixir', 'Eolis', LocationType.shop, ItemType.unknown), + LocationDef(400105, 'Eolis Deluge', 'Eolis', LocationType.shop, ItemType.unknown), + + # Path to Apolune + LocationDef(400106, 'Path to Apolune Magic Shield', 'Path to Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400107, 'Path to Apolune Death', 'Path to Apolune', LocationType.shop, ItemType.unknown), + + # Apolune + LocationDef(400108, 'Apolune Small Shield', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400109, 'Apolune Hand Dagger', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400110, 'Apolune Deluge', 'Apolune', LocationType.shop, ItemType.unknown), + LocationDef(400111, 'Apolune Red Potion', 'Apolune', LocationType.shop, ItemType.red_potion), + LocationDef(400112, 'Apolune Key Jack', 'Apolune', LocationType.shop, ItemType.unknown), + + # Tower of Trunk + LocationDef(400113, 'Tower of Trunk Hidden Mattock', 'Tower of Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400114, 'Tower of Trunk Hidden Hourglass', 'Tower of Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400115, 'Tower of Trunk Boss Mattock', 'Tower of Trunk', LocationType.boss_reward, ItemType.unknown), + + # Path to Forepaw + LocationDef(400116, 'Path to Forepaw Hidden Red Potion', 'Path to Forepaw', LocationType.hidden, ItemType.red_potion), + LocationDef(400117, 'Path to Forepaw Glove', 'Path to Forepaw', LocationType.world, ItemType.unknown), + + # Forepaw + LocationDef(400118, 'Forepaw Long Sword', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400119, 'Forepaw Studded Mail', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400120, 'Forepaw Small Shield', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400121, 'Forepaw Red Potion', 'Forepaw', LocationType.shop, ItemType.red_potion), + LocationDef(400122, 'Forepaw Wingboots', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400123, 'Forepaw Key Jack', 'Forepaw', LocationType.shop, ItemType.unknown), + LocationDef(400124, 'Forepaw Key Queen', 'Forepaw', LocationType.shop, ItemType.unknown), + + # Trunk + LocationDef(400125, 'Trunk Hidden Ointment', 'Trunk', LocationType.hidden, ItemType.unknown), + LocationDef(400126, 'Trunk Hidden Red Potion', 'Trunk', LocationType.hidden, ItemType.red_potion), + LocationDef(400127, 'Trunk Red Potion', 'Trunk', LocationType.world, ItemType.red_potion), + LocationDef(None, 'Sky Spring', 'Trunk', LocationType.spring, ItemType.unknown), + + # Joker Spring + LocationDef(400128, 'Joker Spring Ruby Ring', 'Joker Spring', LocationType.give, ItemType.unknown), + LocationDef(None, 'Joker Spring', 'Joker Spring', LocationType.spring, ItemType.unknown), + + # Tower of Fortress + LocationDef(400129, 'Tower of Fortress Poison 1', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400130, 'Tower of Fortress Poison 2', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400131, 'Tower of Fortress Hidden Wingboots', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400132, 'Tower of Fortress Ointment', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400133, 'Tower of Fortress Boss Wingboots', 'Tower of Fortress', LocationType.boss_reward, ItemType.unknown), + LocationDef(400134, 'Tower of Fortress Elixir', 'Tower of Fortress', LocationType.world, ItemType.unknown), + LocationDef(400135, 'Tower of Fortress Guru', 'Tower of Fortress', LocationType.give, ItemType.unknown), + LocationDef(None, 'Tower of Fortress Spring', 'Tower of Fortress', LocationType.spring, ItemType.unknown), + + # Path to Mascon + LocationDef(400136, 'Path to Mascon Hidden Wingboots', 'Path to Mascon', LocationType.hidden, ItemType.unknown), + + # Tower of Red Potion + LocationDef(400137, 'Tower of Red Potion', 'Tower of Red Potion', LocationType.world, ItemType.red_potion), + + # Mascon + LocationDef(400138, 'Mascon Large Shield', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400139, 'Mascon Thunder', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400140, 'Mascon Mattock', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400141, 'Mascon Red Potion', 'Mascon', LocationType.shop, ItemType.red_potion), + LocationDef(400142, 'Mascon Key Jack', 'Mascon', LocationType.shop, ItemType.unknown), + LocationDef(400143, 'Mascon Key Queen', 'Mascon', LocationType.shop, ItemType.unknown), + + # Path to Victim + LocationDef(400144, 'Misty Shop Death', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400145, 'Misty Shop Hourglass', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400146, 'Misty Shop Elixir', 'Path to Victim', LocationType.shop, ItemType.unknown), + LocationDef(400147, 'Misty Shop Red Potion', 'Path to Victim', LocationType.shop, ItemType.red_potion), + LocationDef(400148, 'Misty Doctor Office', 'Path to Victim', LocationType.hidden, ItemType.unknown), + + # Tower of Suffer + LocationDef(400149, 'Tower of Suffer Hidden Wingboots', 'Tower of Suffer', LocationType.hidden, ItemType.unknown), + LocationDef(400150, 'Tower of Suffer Hidden Hourglass', 'Tower of Suffer', LocationType.hidden, ItemType.unknown), + LocationDef(400151, 'Tower of Suffer Pendant', 'Tower of Suffer', LocationType.boss_reward, ItemType.unknown), + + # Victim + LocationDef(400152, 'Victim Full Plate', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400153, 'Victim Mattock', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400154, 'Victim Red Potion', 'Victim', LocationType.shop, ItemType.red_potion), + LocationDef(400155, 'Victim Key King', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400156, 'Victim Key Queen', 'Victim', LocationType.shop, ItemType.unknown), + LocationDef(400157, 'Victim Tavern', 'Mist', LocationType.give, ItemType.unknown), + + # Mist + LocationDef(400158, 'Mist Hidden Poison 1', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400159, 'Mist Hidden Poison 2', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400160, 'Mist Hidden Wingboots', 'Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400161, 'Misty Magic Hall', 'Mist', LocationType.give, ItemType.unknown), + LocationDef(400162, 'Misty House', 'Mist', LocationType.give, ItemType.unknown), + + # Useless Tower + LocationDef(400163, 'Useless Tower', 'Useless Tower', LocationType.hidden, ItemType.unknown), + + # Tower of Mist + LocationDef(400164, 'Tower of Mist Hidden Ointment', 'Tower of Mist', LocationType.hidden, ItemType.unknown), + LocationDef(400165, 'Tower of Mist Elixir', 'Tower of Mist', LocationType.world, ItemType.unknown), + LocationDef(400166, 'Tower of Mist Black Onyx', 'Tower of Mist', LocationType.boss_reward, ItemType.unknown), + + # Path to Conflate + LocationDef(400167, 'Path to Conflate Hidden Ointment', 'Path to Conflate', LocationType.hidden, ItemType.unknown), + LocationDef(400168, 'Path to Conflate Poison', 'Path to Conflate', LocationType.hidden, ItemType.unknown), + + # Helm Branch + LocationDef(400169, 'Helm Branch Hidden Glove', 'Helm Branch', LocationType.hidden, ItemType.unknown), + LocationDef(400170, 'Helm Branch Battle Helmet', 'Helm Branch', LocationType.boss_reward, ItemType.unknown), + + # Conflate + LocationDef(400171, 'Conflate Giant Blade', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400172, 'Conflate Magic Shield', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400173, 'Conflate Wingboots', 'Conflate', LocationType.shop, ItemType.unknown), + LocationDef(400174, 'Conflate Red Potion', 'Conflate', LocationType.shop, ItemType.red_potion), + LocationDef(400175, 'Conflate Guru', 'Conflate', LocationType.give, ItemType.unknown), + + # Branches + LocationDef(400176, 'Branches Hidden Ointment', 'Branches', LocationType.hidden, ItemType.unknown), + LocationDef(400177, 'Branches Poison', 'Branches', LocationType.world, ItemType.unknown), + LocationDef(400178, 'Branches Hidden Mattock', 'Branches', LocationType.hidden, ItemType.unknown), + LocationDef(400179, 'Branches Hidden Hourglass', 'Branches', LocationType.hidden, ItemType.unknown), + + # Path to Daybreak + LocationDef(400180, 'Path to Daybreak Hidden Wingboots 1', 'Path to Daybreak', LocationType.hidden, ItemType.unknown), + LocationDef(400181, 'Path to Daybreak Magical Rod', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400182, 'Path to Daybreak Hidden Wingboots 2', 'Path to Daybreak', LocationType.hidden, ItemType.unknown), + LocationDef(400183, 'Path to Daybreak Poison', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400184, 'Path to Daybreak Glove', 'Path to Daybreak', LocationType.world, ItemType.unknown), + LocationDef(400185, 'Path to Daybreak Battle Suit', 'Path to Daybreak', LocationType.boss_reward, ItemType.unknown), + + # Daybreak + LocationDef(400186, 'Daybreak Tilte', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400187, 'Daybreak Giant Blade', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400188, 'Daybreak Red Potion', 'Daybreak', LocationType.shop, ItemType.red_potion), + LocationDef(400189, 'Daybreak Key King', 'Daybreak', LocationType.shop, ItemType.unknown), + LocationDef(400190, 'Daybreak Key Queen', 'Daybreak', LocationType.shop, ItemType.unknown), + + # Dartmoor Castle + LocationDef(400191, 'Dartmoor Castle Hidden Hourglass', 'Dartmoor Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400192, 'Dartmoor Castle Hidden Red Potion', 'Dartmoor Castle', LocationType.hidden, ItemType.red_potion), + + # Dartmoor + LocationDef(400193, 'Dartmoor Giant Blade', 'Dartmoor', LocationType.shop, ItemType.unknown), + LocationDef(400194, 'Dartmoor Red Potion', 'Dartmoor', LocationType.shop, ItemType.red_potion), + LocationDef(400195, 'Dartmoor Key King', 'Dartmoor', LocationType.shop, ItemType.unknown), + + # Fraternal Castle + LocationDef(400196, 'Fraternal Castle Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400197, 'Fraternal Castle Shop Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400198, 'Fraternal Castle Poison 1', 'Fraternal Castle', LocationType.world, ItemType.unknown), + LocationDef(400199, 'Fraternal Castle Poison 2', 'Fraternal Castle', LocationType.world, ItemType.unknown), + LocationDef(400200, 'Fraternal Castle Poison 3', 'Fraternal Castle', LocationType.world, ItemType.unknown), + # LocationDef(400201, 'Fraternal Castle Red Potion', 'Fraternal Castle', LocationType.world, ItemType.red_potion), # This location is inaccessible. Keeping commented for context. + LocationDef(400202, 'Fraternal Castle Hidden Hourglass', 'Fraternal Castle', LocationType.hidden, ItemType.unknown), + LocationDef(400203, 'Fraternal Castle Dragon Slayer', 'Fraternal Castle', LocationType.boss_reward, ItemType.unknown), + LocationDef(400204, 'Fraternal Castle Guru', 'Fraternal Castle', LocationType.give, ItemType.unknown), + + # Evil Fortress + LocationDef(400205, 'Evil Fortress Ointment', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400206, 'Evil Fortress Poison 1', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400207, 'Evil Fortress Glove', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400208, 'Evil Fortress Poison 2', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400209, 'Evil Fortress Poison 3', 'Evil Fortress', LocationType.world, ItemType.unknown), + LocationDef(400210, 'Evil Fortress Hidden Glove', 'Evil Fortress', LocationType.hidden, ItemType.unknown), + LocationDef(None, 'Evil One', 'Evil Fortress', LocationType.boss, ItemType.unknown), +] diff --git a/worlds/faxanadu/Options.py b/worlds/faxanadu/Options.py new file mode 100644 index 000000000000..dbcb5789944f --- /dev/null +++ b/worlds/faxanadu/Options.py @@ -0,0 +1,107 @@ +from Options import PerGameCommonOptions, Toggle, DefaultOnToggle, StartInventoryPool, Choice +from dataclasses import dataclass + + +class KeepShopRedPotions(Toggle): + """ + Prevents the Shop's Red Potions from being shuffled. Those locations + will have purchasable Red Potion as usual for their usual price. + """ + display_name = "Keep Shop Red Potions" + + +class IncludePendant(Toggle): + """ + Pendant is an item that boosts your attack power permanently when picked up. + However, due to a programming error in the original game, it has the reverse + effect. You start with the Pendant power, and lose it when picking + it up. So this item is essentially a trap. + There is a setting in the client to reverse the effect back to its original intend. + This could be used in conjunction with this option to increase or lower difficulty. + """ + display_name = "Include Pendant" + + +class IncludePoisons(DefaultOnToggle): + """ + Whether or not to include Poison Potions in the pool of items. Including them + effectively turn them into traps in multiplayer. + """ + display_name = "Include Poisons" + + +class RequireDragonSlayer(Toggle): + """ + Requires the Dragon Slayer to be available before fighting the final boss is required. + Turning this on will turn Progressive Shields into progression items. + + This setting does not force you to use Dragon Slayer to kill the final boss. + Instead, it ensures that you will have the Dragon Slayer and be able to equip + it before you are expected to beat the final boss. + """ + display_name = "Require Dragon Slayer" + + +class RandomMusic(Toggle): + """ + All levels' music is shuffled. Except the title screen because it's finite. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random Musics" + + +class RandomSound(Toggle): + """ + All sounds are shuffled. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random Sounds" + + +class RandomNPC(Toggle): + """ + NPCs and their portraits are shuffled. + This is an aesthetic option and doesn't affect gameplay. + """ + display_name = "Random NPCs" + + +class RandomMonsters(Choice): + """ + Choose how monsters are randomized. + "Vanilla": No randomization + "Level Shuffle": Monsters are shuffled within a level + "Level Random": Monsters are picked randomly, balanced based on the ratio of the current level + "World Shuffle": Monsters are shuffled across the entire world + "World Random": Monsters are picked randomly, balanced based on the ratio of the entire world + "Chaotic": Completely random, except big vs small ratio is kept. Big are mini-bosses. + """ + display_name = "Random Monsters" + option_vanilla = 0 + option_level_shuffle = 1 + option_level_random = 2 + option_world_shuffle = 3 + option_world_random = 4 + option_chaotic = 5 + default = 0 + + +class RandomRewards(Toggle): + """ + Monsters drops are shuffled. + """ + display_name = "Random Rewards" + + +@dataclass +class FaxanaduOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + keep_shop_red_potions: KeepShopRedPotions + include_pendant: IncludePendant + include_poisons: IncludePoisons + require_dragon_slayer: RequireDragonSlayer + random_musics: RandomMusic + random_sounds: RandomSound + random_npcs: RandomNPC + random_monsters: RandomMonsters + random_rewards: RandomRewards diff --git a/worlds/faxanadu/Regions.py b/worlds/faxanadu/Regions.py new file mode 100644 index 000000000000..9db11d8ef114 --- /dev/null +++ b/worlds/faxanadu/Regions.py @@ -0,0 +1,66 @@ +from BaseClasses import Region +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import FaxanaduWorld + + +def create_region(name, player, multiworld): + region = Region(name, player, multiworld) + multiworld.regions.append(region) + return region + + +def create_regions(faxanadu_world: "FaxanaduWorld"): + player = faxanadu_world.player + multiworld = faxanadu_world.multiworld + + # Create regions + menu = create_region("Menu", player, multiworld) + eolis = create_region("Eolis", player, multiworld) + path_to_apolune = create_region("Path to Apolune", player, multiworld) + apolune = create_region("Apolune", player, multiworld) + create_region("Tower of Trunk", player, multiworld) + path_to_forepaw = create_region("Path to Forepaw", player, multiworld) + forepaw = create_region("Forepaw", player, multiworld) + trunk = create_region("Trunk", player, multiworld) + create_region("Joker Spring", player, multiworld) + create_region("Tower of Fortress", player, multiworld) + path_to_mascon = create_region("Path to Mascon", player, multiworld) + create_region("Tower of Red Potion", player, multiworld) + mascon = create_region("Mascon", player, multiworld) + path_to_victim = create_region("Path to Victim", player, multiworld) + create_region("Tower of Suffer", player, multiworld) + victim = create_region("Victim", player, multiworld) + mist = create_region("Mist", player, multiworld) + create_region("Useless Tower", player, multiworld) + create_region("Tower of Mist", player, multiworld) + path_to_conflate = create_region("Path to Conflate", player, multiworld) + create_region("Helm Branch", player, multiworld) + create_region("Conflate", player, multiworld) + branches = create_region("Branches", player, multiworld) + path_to_daybreak = create_region("Path to Daybreak", player, multiworld) + daybreak = create_region("Daybreak", player, multiworld) + dartmoor_castle = create_region("Dartmoor Castle", player, multiworld) + create_region("Dartmoor", player, multiworld) + create_region("Fraternal Castle", player, multiworld) + create_region("Evil Fortress", player, multiworld) + + # Create connections + menu.add_exits(["Eolis"]) + eolis.add_exits(["Path to Apolune"]) + path_to_apolune.add_exits(["Apolune"]) + apolune.add_exits(["Tower of Trunk", "Path to Forepaw"]) + path_to_forepaw.add_exits(["Forepaw"]) + forepaw.add_exits(["Trunk"]) + trunk.add_exits(["Joker Spring", "Tower of Fortress", "Path to Mascon"]) + path_to_mascon.add_exits(["Tower of Red Potion", "Mascon"]) + mascon.add_exits(["Path to Victim"]) + path_to_victim.add_exits(["Tower of Suffer", "Victim"]) + victim.add_exits(["Mist"]) + mist.add_exits(["Useless Tower", "Tower of Mist", "Path to Conflate"]) + path_to_conflate.add_exits(["Helm Branch", "Conflate", "Branches"]) + branches.add_exits(["Path to Daybreak"]) + path_to_daybreak.add_exits(["Daybreak"]) + daybreak.add_exits(["Dartmoor Castle"]) + dartmoor_castle.add_exits(["Dartmoor", "Fraternal Castle", "Evil Fortress"]) diff --git a/worlds/faxanadu/Rules.py b/worlds/faxanadu/Rules.py new file mode 100644 index 000000000000..a48b442c107a --- /dev/null +++ b/worlds/faxanadu/Rules.py @@ -0,0 +1,79 @@ +from typing import TYPE_CHECKING +from worlds.generic.Rules import set_rule + +if TYPE_CHECKING: + from . import FaxanaduWorld + + +def can_buy_in_eolis(state, player): + # Sword or Deluge so we can farm for gold. + # Ring of Elf so we can get 1500 from the King. + return state.has_any(["Progressive Sword", "Deluge", "Ring of Elf"], player) + + +def has_any_magic(state, player): + return state.has_any(["Deluge", "Thunder", "Fire", "Death", "Tilte"], player) + + +def set_rules(faxanadu_world: "FaxanaduWorld"): + player = faxanadu_world.player + multiworld = faxanadu_world.multiworld + + # Region rules + set_rule(multiworld.get_entrance("Eolis -> Path to Apolune", player), lambda state: + state.has_all(["Key Jack", "Progressive Sword"], player)) # You can't go far with magic only + set_rule(multiworld.get_entrance("Apolune -> Tower of Trunk", player), lambda state: state.has("Key Jack", player)) + set_rule(multiworld.get_entrance("Apolune -> Path to Forepaw", player), lambda state: state.has("Mattock", player)) + set_rule(multiworld.get_entrance("Trunk -> Joker Spring", player), lambda state: state.has("Key Joker", player)) + set_rule(multiworld.get_entrance("Trunk -> Tower of Fortress", player), lambda state: state.has("Key Jack", player)) + set_rule(multiworld.get_entrance("Trunk -> Path to Mascon", player), lambda state: + state.has_all(["Key Queen", "Ring of Ruby", "Sky Spring Flow", "Tower of Fortress Spring Flow", "Joker Spring Flow"], player) and + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_entrance("Path to Mascon -> Tower of Red Potion", player), lambda state: + state.has("Key Queen", player) and + state.has("Red Potion", player, 4)) # It's impossible to go through the tower of Red Potion without at least 1-2 potions. Give them 4 for good measure. + set_rule(multiworld.get_entrance("Path to Victim -> Tower of Suffer", player), lambda state: state.has("Key Queen", player)) + set_rule(multiworld.get_entrance("Path to Victim -> Victim", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_entrance("Mist -> Useless Tower", player), lambda state: + state.has_all(["Key King", "Unlock Wingboots"], player)) + set_rule(multiworld.get_entrance("Mist -> Tower of Mist", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Mist -> Path to Conflate", player), lambda state: state.has("Key Ace", player)) + set_rule(multiworld.get_entrance("Path to Conflate -> Helm Branch", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Path to Conflate -> Branches", player), lambda state: state.has("Key King", player)) + set_rule(multiworld.get_entrance("Daybreak -> Dartmoor Castle", player), lambda state: state.has("Ring of Dworf", player)) + set_rule(multiworld.get_entrance("Dartmoor Castle -> Evil Fortress", player), lambda state: state.has("Demons Ring", player)) + + # Location rules + set_rule(multiworld.get_location("Eolis Key Jack", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Hand Dagger", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Elixir", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Deluge", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Eolis Red Potion", player), lambda state: can_buy_in_eolis(state, player)) + set_rule(multiworld.get_location("Path to Apolune Magic Shield", player), lambda state: state.has("Key King", player)) # Mid-late cost, make sure we've progressed + set_rule(multiworld.get_location("Path to Apolune Death", player), lambda state: state.has("Key Ace", player)) # Mid-late cost, make sure we've progressed + set_rule(multiworld.get_location("Tower of Trunk Hidden Mattock", player), lambda state: + # This is actually possible if the monster drop into the stairs and kill it with dagger. But it's a "pro move" + state.has("Deluge", player, 1) or + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_location("Path to Forepaw Glove", player), lambda state: + state.has_all(["Deluge", "Unlock Wingboots"], player)) + set_rule(multiworld.get_location("Trunk Red Potion", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Sky Spring", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Tower of Fortress Spring", player), lambda state: state.has("Spring Elixir", player)) + set_rule(multiworld.get_location("Tower of Fortress Guru", player), lambda state: state.has("Sky Spring Flow", player)) + set_rule(multiworld.get_location("Tower of Suffer Hidden Wingboots", player), lambda state: + state.has("Deluge", player) or + state.has("Progressive Sword", player, 2)) + set_rule(multiworld.get_location("Misty House", player), lambda state: state.has("Black Onyx", player)) + set_rule(multiworld.get_location("Misty Doctor Office", player), lambda state: has_any_magic(state, player)) + set_rule(multiworld.get_location("Conflate Guru", player), lambda state: state.has("Progressive Armor", player, 3)) + set_rule(multiworld.get_location("Branches Hidden Mattock", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Path to Daybreak Glove", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Dartmoor Castle Hidden Hourglass", player), lambda state: state.has("Unlock Wingboots", player)) + set_rule(multiworld.get_location("Dartmoor Castle Hidden Red Potion", player), lambda state: has_any_magic(state, player)) + set_rule(multiworld.get_location("Fraternal Castle Guru", player), lambda state: state.has("Progressive Sword", player, 4)) + set_rule(multiworld.get_location("Fraternal Castle Shop Hidden Ointment", player), lambda state: has_any_magic(state, player)) + + if faxanadu_world.options.require_dragon_slayer.value: + set_rule(multiworld.get_location("Evil One", player), lambda state: + state.has_all_counts({"Progressive Sword": 4, "Progressive Armor": 3, "Progressive Shield": 4}, player)) diff --git a/worlds/faxanadu/__init__.py b/worlds/faxanadu/__init__.py new file mode 100644 index 000000000000..c4ae1ccaa198 --- /dev/null +++ b/worlds/faxanadu/__init__.py @@ -0,0 +1,190 @@ +from typing import Any, Dict, List + +from BaseClasses import Item, Location, Tutorial, ItemClassification, MultiWorld +from worlds.AutoWorld import WebWorld, World +from . import Items, Locations, Regions, Rules +from .Options import FaxanaduOptions +from worlds.generic.Rules import set_rule + + +DAXANADU_VERSION = "0.3.0" + + +class FaxanaduLocation(Location): + game: str = "Faxanadu" + + +class FaxanaduItem(Item): + game: str = "Faxanadu" + + +class FaxanaduWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Faxanadu randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Daivuk"] + )] + theme = "dirt" + + +class FaxanaduWorld(World): + """ + Faxanadu is an action role-playing platform video game developed by Hudson Soft for the Nintendo Entertainment System + """ + options_dataclass = FaxanaduOptions + options: FaxanaduOptions + game = "Faxanadu" + web = FaxanaduWeb() + + item_name_to_id = {item.name: item.id for item in Items.items if item.id is not None} + item_name_to_item = {item.name: item for item in Items.items} + location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None} + + def __init__(self, world: MultiWorld, player: int): + self.filler_ratios: Dict[str, int] = {} + + super().__init__(world, player) + + def create_regions(self): + Regions.create_regions(self) + + # Add locations into regions + for region in self.multiworld.get_regions(self.player): + for loc in [location for location in Locations.locations if location.region == region.name]: + location = FaxanaduLocation(self.player, loc.name, loc.id, region) + + # In Faxanadu, Poison hurts you when picked up. It makes no sense to sell them in shops + if loc.type == Locations.LocationType.shop: + location.item_rule = lambda item, player=self.player: not (player == item.player and item.name == "Poison") + + region.locations.append(location) + + def set_rules(self): + Rules.set_rules(self) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Killed Evil One", self.player) + + def create_item(self, name: str) -> FaxanaduItem: + item: Items.ItemDef = self.item_name_to_item[name] + return FaxanaduItem(name, item.classification, item.id, self.player) + + # Returns how many red potions were prefilled into shops + def prefill_shop_red_potions(self) -> int: + red_potion_in_shop_count = 0 + if self.options.keep_shop_red_potions: + red_potion_item = self.item_name_to_item["Red Potion"] + red_potion_shop_locations = [ + loc + for loc in Locations.locations + if loc.type == Locations.LocationType.shop and loc.original_item == Locations.ItemType.red_potion + ] + for loc in red_potion_shop_locations: + location = self.get_location(loc.name) + location.place_locked_item(FaxanaduItem(red_potion_item.name, red_potion_item.classification, red_potion_item.id, self.player)) + red_potion_in_shop_count += 1 + return red_potion_in_shop_count + + def put_wingboot_in_shop(self, shops, region_name): + item = self.item_name_to_item["Wingboots"] + shop = shops.pop(region_name) + slot = self.random.randint(0, len(shop) - 1) + loc = shop[slot] + location = self.get_location(loc.name) + location.place_locked_item(FaxanaduItem(item.name, item.classification, item.id, self.player)) + + # Put a rule right away that we need to have to unlocked. + set_rule(location, lambda state: state.has("Unlock Wingboots", self.player)) + + # Returns how many wingboots were prefilled into shops + def prefill_shop_wingboots(self) -> int: + # Collect shops + shops: Dict[str, List[Locations.LocationDef]] = {} + for loc in Locations.locations: + if loc.type == Locations.LocationType.shop: + if self.options.keep_shop_red_potions and loc.original_item == Locations.ItemType.red_potion: + continue # Don't override our red potions + shops.setdefault(loc.region, []).append(loc) + + shop_count = len(shops) + wingboots_count = round(shop_count / 2.5) # On 10 shops, we should have about 4 shops with wingboots + + # At least one should be in the first 4 shops. Because we require wingboots to progress past that point. + must_have_regions = [region for i, region in enumerate(shops) if i < 4] + self.put_wingboot_in_shop(shops, self.random.choice(must_have_regions)) + + # Fill in the rest randomly in remaining shops + for i in range(wingboots_count - 1): # -1 because we added one already + region = self.random.choice(list(shops.keys())) + self.put_wingboot_in_shop(shops, region) + + return wingboots_count + + def create_items(self) -> None: + itempool: List[FaxanaduItem] = [] + + # Prefill red potions in shops if option is set + red_potion_in_shop_count = self.prefill_shop_red_potions() + + # Prefill wingboots in shops + wingboots_in_shop_count = self.prefill_shop_wingboots() + + # Create the item pool, excluding fillers. + prefilled_count = red_potion_in_shop_count + wingboots_in_shop_count + for item in Items.items: + # Ignore pendant if turned off + if item.name == "Pendant" and not self.options.include_pendant: + continue + + # ignore fillers for now, we will fill them later + if item.classification in [ItemClassification.filler, ItemClassification.trap] and \ + item.progression_count == 0: + continue + + prefill_loc = None + if item.prefill_location: + prefill_loc = self.get_location(item.prefill_location) + + # if require dragon slayer is turned on, we need progressive shields to be progression + item_classification = item.classification + if self.options.require_dragon_slayer and item.name == "Progressive Shield": + item_classification = ItemClassification.progression + + if prefill_loc: + prefill_loc.place_locked_item(FaxanaduItem(item.name, item_classification, item.id, self.player)) + prefilled_count += 1 + else: + for i in range(item.count - item.progression_count): + itempool.append(FaxanaduItem(item.name, item_classification, item.id, self.player)) + for i in range(item.progression_count): + itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player)) + + # Set up filler ratios + self.filler_ratios = { + item.name: item.count + for item in Items.items + if item.classification in [ItemClassification.filler, ItemClassification.trap] + } + + # If red potions are locked in shops, remove the count from the ratio. + self.filler_ratios["Red Potion"] -= red_potion_in_shop_count + + # Remove poisons if not desired + if not self.options.include_poisons: + self.filler_ratios["Poison"] = 0 + + # Randomly add fillers to the pool with ratios based on og game occurrence counts. + filler_count = len(Locations.locations) - len(itempool) - prefilled_count + for i in range(filler_count): + itempool.append(self.create_item(self.get_filler_item_name())) + + self.multiworld.itempool += itempool + + def get_filler_item_name(self) -> str: + return self.random.choices(list(self.filler_ratios.keys()), weights=list(self.filler_ratios.values()))[0] + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict("keep_shop_red_potions", "random_musics", "random_sounds", "random_npcs", "random_monsters", "random_rewards") + slot_data["daxanadu_version"] = DAXANADU_VERSION + return slot_data diff --git a/worlds/faxanadu/docs/en_Faxanadu.md b/worlds/faxanadu/docs/en_Faxanadu.md new file mode 100644 index 000000000000..7f5c4ab293ce --- /dev/null +++ b/worlds/faxanadu/docs/en_Faxanadu.md @@ -0,0 +1,27 @@ +# Faxanadu + +## Where is the settings page? + +The [player options page](../player-options) contains the options needed to configure your game session. + +## What does randomization do to this game? + +All game items collected in the map, shops, and boss drops are randomized. + +Keys are unique. Once you get the Jack Key, you can open all Jack doors; the key stays in your inventory. + +Wingboots are randomized across shops only. They are LOCKED and cannot be bought until you get the item that unlocks them. + +Normal Elixirs don't revive the tower spring. A new item, Spring Elixir, is necessary. This new item is unique. + +## What is the goal? + +The goal is to kill the Evil One. + +## What is a "check" in The Faxanadu? + +Shop items, item locations in the world, boss drops, and secret items. + +## What "items" can you unlock in Faxanadu? + +Keys, Armors, Weapons, Potions, Shields, Magics, Poisons, Gloves, etc. diff --git a/worlds/faxanadu/docs/setup_en.md b/worlds/faxanadu/docs/setup_en.md new file mode 100644 index 000000000000..4ff714c61393 --- /dev/null +++ b/worlds/faxanadu/docs/setup_en.md @@ -0,0 +1,32 @@ +# Faxanadu Randomizer Setup + +## Required Software + +- [Daxanadu](https://github.com/Daivuk/Daxanadu/releases/) +- Faxanadu ROM, English version + +## Optional Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) + +## Installing Daxanadu +1. Download [Daxanadu.zip](https://github.com/Daivuk/Daxanadu/releases/) and extract it. +2. Copy your rom `Faxanadu (U).nes` into the newly extracted folder. + +## Joining a MultiWorld Game + +1. Launch Daxanadu.exe +2. From the Main menu, go to the `ARCHIPELAGO` menu. Enter the server's address, slot name, and password. Then select `PLAY`. +3. Enjoy! + +To continue a game, follow the same connection steps. +Connecting with a different seed won't erase your progress in other seeds. + +## Archipelago Text Client + +We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. +Daxanadu doesn't display messages. You'll only get popups when picking them up. + +## Auto-Tracking + +Daxanadu has an integrated tracker that can be toggled in the options. diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 7de486314c6c..401c240a46ba 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -47,6 +47,17 @@ def get_flag(data, flag): bit = int(0x80 / (2 ** (flag % 8))) return (data[byte] & bit) > 0 +def validate_read_state(data1, data2): + validation_array = bytes([0x01, 0x46, 0x46, 0x4D, 0x51, 0x52]) + + if data1 is None or data2 is None: + return False + for i in range(6): + if data1[i] != validation_array[i] or data2[i] != validation_array[i]: + return False; + return True + + class FFMQClient(SNIClient): game = "Final Fantasy Mystic Quest" @@ -67,11 +78,11 @@ async def validate_rom(self, ctx): async def game_watcher(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - check_1 = await snes_read(ctx, 0xF53749, 1) + check_1 = await snes_read(ctx, 0xF53749, 6) received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) - check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'): + check_2 = await snes_read(ctx, 0xF53749, 6) + if not validate_read_state(check_1, check_2): return def get_range(data_range): diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py index d0898d7e81c8..f1c102d34ef8 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -222,10 +222,10 @@ def yaml_item(text): def create_items(self) -> None: items = [] - starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") + starting_weapon = self.options.starting_weapon.current_key.title().replace("_", " ") self.multiworld.push_precollected(self.create_item(starting_weapon)) self.multiworld.push_precollected(self.create_item("Steel Armor")) - if self.multiworld.sky_coin_mode[self.player] == "start_with": + if self.options.sky_coin_mode == "start_with": self.multiworld.push_precollected(self.create_item("Sky Coin")) precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} @@ -233,28 +233,28 @@ def create_items(self) -> None: def add_item(item_name): if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name: return - if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key: + if item_name.lower().replace(" ", "_") == self.options.starting_weapon.current_key: return - if self.multiworld.progressive_gear[self.player]: + if self.options.progressive_gear: for item_group in prog_map: if item_name in self.item_name_groups[item_group]: item_name = prog_map[item_group] break if item_name == "Sky Coin": - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": for _ in range(40): items.append(self.create_item("Sky Fragment")) return - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": items.append(self.create_filler()) return if item_name in precollected_item_names: items.append(self.create_filler()) return i = self.create_item(item_name) - if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"): + if self.options.logic != "friendly" and item_name in ("Magic Mirror", "Mask"): i.classification = ItemClassification.useful - if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and + if (self.options.logic == "expert" and self.options.map_shuffle == "none" and item_name == "Exit Book"): i.classification = ItemClassification.progression items.append(i) @@ -263,11 +263,11 @@ def add_item(item_name): for item in self.item_name_groups[item_group]: add_item(item) - if self.multiworld.brown_boxes[self.player] == "include": + if self.options.brown_boxes == "include": filler_items = [] for item, count in fillers.items(): filler_items += [self.create_item(item) for _ in range(count)] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": self.multiworld.random.shuffle(filler_items) filler_items = filler_items[39:] items += filler_items diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index af3625f28a9d..41c397315f87 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -1,4 +1,5 @@ -from Options import Choice, FreeText, Toggle, Range +from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions +from dataclasses import dataclass class Logic(Choice): @@ -321,36 +322,36 @@ class KaelisMomFightsMinotaur(Toggle): default = 0 -option_definitions = { - "logic": Logic, - "brown_boxes": BrownBoxes, - "sky_coin_mode": SkyCoinMode, - "shattered_sky_coin_quantity": ShatteredSkyCoinQuantity, - "starting_weapon": StartingWeapon, - "progressive_gear": ProgressiveGear, - "leveling_curve": LevelingCurve, - "starting_companion": StartingCompanion, - "available_companions": AvailableCompanions, - "companions_locations": CompanionsLocations, - "kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur, - "companion_leveling_type": CompanionLevelingType, - "companion_spellbook_type": CompanionSpellbookType, - "enemies_density": EnemiesDensity, - "enemies_scaling_lower": EnemiesScalingLower, - "enemies_scaling_upper": EnemiesScalingUpper, - "bosses_scaling_lower": BossesScalingLower, - "bosses_scaling_upper": BossesScalingUpper, - "enemizer_attacks": EnemizerAttacks, - "enemizer_groups": EnemizerGroups, - "shuffle_res_weak_types": ShuffleResWeakType, - "shuffle_enemies_position": ShuffleEnemiesPositions, - "progressive_formations": ProgressiveFormations, - "doom_castle_mode": DoomCastle, - "doom_castle_shortcut": DoomCastleShortcut, - "tweak_frustrating_dungeons": TweakFrustratingDungeons, - "map_shuffle": MapShuffle, - "crest_shuffle": CrestShuffle, - "shuffle_battlefield_rewards": ShuffleBattlefieldRewards, - "map_shuffle_seed": MapShuffleSeed, - "battlefields_battles_quantities": BattlefieldsBattlesQuantities, -} +@dataclass +class FFMQOptions(PerGameCommonOptions): + logic: Logic + brown_boxes: BrownBoxes + sky_coin_mode: SkyCoinMode + shattered_sky_coin_quantity: ShatteredSkyCoinQuantity + starting_weapon: StartingWeapon + progressive_gear: ProgressiveGear + leveling_curve: LevelingCurve + starting_companion: StartingCompanion + available_companions: AvailableCompanions + companions_locations: CompanionsLocations + kaelis_mom_fight_minotaur: KaelisMomFightsMinotaur + companion_leveling_type: CompanionLevelingType + companion_spellbook_type: CompanionSpellbookType + enemies_density: EnemiesDensity + enemies_scaling_lower: EnemiesScalingLower + enemies_scaling_upper: EnemiesScalingUpper + bosses_scaling_lower: BossesScalingLower + bosses_scaling_upper: BossesScalingUpper + enemizer_attacks: EnemizerAttacks + enemizer_groups: EnemizerGroups + shuffle_res_weak_types: ShuffleResWeakType + shuffle_enemies_position: ShuffleEnemiesPositions + progressive_formations: ProgressiveFormations + doom_castle_mode: DoomCastle + doom_castle_shortcut: DoomCastleShortcut + tweak_frustrating_dungeons: TweakFrustratingDungeons + map_shuffle: MapShuffle + crest_shuffle: CrestShuffle + shuffle_battlefield_rewards: ShuffleBattlefieldRewards + map_shuffle_seed: MapShuffleSeed + battlefields_battles_quantities: BattlefieldsBattlesQuantities diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py index 1b17aaa98f28..1e436a90c5fd 100644 --- a/worlds/ffmq/Output.py +++ b/worlds/ffmq/Output.py @@ -1,13 +1,13 @@ import yaml import os import zipfile +import Utils from copy import deepcopy from .Regions import object_id_table -from Utils import __version__ from worlds.Files import APPatch import pkgutil -settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader) +settings_template = Utils.parse_yaml(pkgutil.get_data(__name__, "data/settings.yaml")) def generate_output(self, output_directory): @@ -21,7 +21,7 @@ def output_item_name(item): item_name = "".join(item_name.split(" ")) else: if item.advancement or item.useful or (item.trap and - self.multiworld.per_slot_randoms[self.player].randint(0, 1)): + self.random.randint(0, 1)): item_name = "APItem" else: item_name = "APItemFiller" @@ -46,60 +46,60 @@ def tf(option): options = deepcopy(settings_template) options["name"] = self.multiworld.player_name[self.player] option_writes = { - "enemies_density": cc(self.multiworld.enemies_density[self.player]), + "enemies_density": cc(self.options.enemies_density), "chests_shuffle": "Include", - "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", + "shuffle_boxes_content": self.options.brown_boxes == "shuffle", "npcs_shuffle": "Include", "battlefields_shuffle": "Include", - "logic_options": cc(self.multiworld.logic[self.player]), - "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), - "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), - "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), - "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), - "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), - "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), - "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), - "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if - self.multiworld.battlefields_battles_quantities[self.player].value < 5 else + "logic_options": cc(self.options.logic), + "shuffle_enemies_position": tf(self.options.shuffle_enemies_position), + "enemies_scaling_lower": cc(self.options.enemies_scaling_lower), + "enemies_scaling_upper": cc(self.options.enemies_scaling_upper), + "bosses_scaling_lower": cc(self.options.bosses_scaling_lower), + "bosses_scaling_upper": cc(self.options.bosses_scaling_upper), + "enemizer_attacks": cc(self.options.enemizer_attacks), + "leveling_curve": cc(self.options.leveling_curve), + "battles_quantity": cc(self.options.battlefields_battles_quantities) if + self.options.battlefields_battles_quantities.value < 5 else "RandomLow" if - self.multiworld.battlefields_battles_quantities[self.player].value == 5 else + self.options.battlefields_battles_quantities.value == 5 else "RandomHigh", - "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), + "shuffle_battlefield_rewards": tf(self.options.shuffle_battlefield_rewards), "random_starting_weapon": True, - "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), - "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), - "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), - "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), - "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), - "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), + "progressive_gear": tf(self.options.progressive_gear), + "tweaked_dungeons": tf(self.options.tweak_frustrating_dungeons), + "doom_castle_mode": cc(self.options.doom_castle_mode), + "doom_castle_shortcut": tf(self.options.doom_castle_shortcut), + "sky_coin_mode": cc(self.options.sky_coin_mode), + "sky_coin_fragments_qty": cc(self.options.shattered_sky_coin_quantity), "enable_spoilers": False, - "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), - "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), - "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), - "enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]), - "shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]), - "companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]), - "companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]), - "starting_companion": cc(self.multiworld.starting_companion[self.player]), + "progressive_formations": cc(self.options.progressive_formations), + "map_shuffling": cc(self.options.map_shuffle), + "crest_shuffle": tf(self.options.crest_shuffle), + "enemizer_groups": cc(self.options.enemizer_groups), + "shuffle_res_weak_type": tf(self.options.shuffle_res_weak_types), + "companion_leveling_type": cc(self.options.companion_leveling_type), + "companion_spellbook_type": cc(self.options.companion_spellbook_type), + "starting_companion": cc(self.options.starting_companion), "available_companions": ["Zero", "One", "Two", - "Three", "Four"][self.multiworld.available_companions[self.player].value], - "companions_locations": cc(self.multiworld.companions_locations[self.player]), - "kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]), + "Three", "Four"][self.options.available_companions.value], + "companions_locations": cc(self.options.companions_locations), + "kaelis_mom_fight_minotaur": tf(self.options.kaelis_mom_fight_minotaur), } for option, data in option_writes.items(): options["Final Fantasy Mystic Quest"][option][data] = 1 - rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] + rom_name = f'MQ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] self.rom_name = bytearray(rom_name, 'utf8') self.rom_name_available_event.set() setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": - hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} + hex(self.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": starting_items.append("SkyCoin") file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq") diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index 8b83c88e72c9..4e26be1653a6 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -1,11 +1,9 @@ from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule +from .data.rooms import rooms, entrances from .Items import item_groups, yaml_item -import pkgutil -import yaml -rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader) -entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)} +entrance_names = {entrance["id"]: entrance["name"] for entrance in entrances} object_id_table = {} object_type_table = {} @@ -69,7 +67,7 @@ def create_regions(self): location_table else None, object["type"], object["access"], self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp", - "BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and + "BattlefieldXp") and (object["type"] != "Box" or self.options.brown_boxes == "include") and not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"])) dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) @@ -91,15 +89,13 @@ def create_regions(self): if "entrance" in link and link["entrance"] != -1: spoiler = False if link["entrance"] in crest_warps: - if self.multiworld.crest_shuffle[self.player]: + if self.options.crest_shuffle: spoiler = True - elif self.multiworld.map_shuffle[self.player] == "everything": + elif self.options.map_shuffle == "everything": spoiler = True - elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons", - "none"): + elif "Subregion" in region.name and self.options.map_shuffle not in ("dungeons", "none"): spoiler = True - elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none", - "overworld"): + elif "Subregion" not in region.name and self.options.map_shuffle not in ("none", "overworld"): spoiler = True if spoiler: @@ -111,6 +107,7 @@ def create_regions(self): connection.connect(connect_room) break + non_dead_end_crest_rooms = [ 'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room', 'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge', @@ -140,7 +137,7 @@ def hard_boss_logic(state): add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic) add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic) - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"): loc = self.multiworld.get_location(boss, self.player) checked_regions = {loc.parent_region} @@ -158,12 +155,12 @@ def check_foresta(region): return True check_foresta(loc.parent_region) - if self.multiworld.logic[self.player] == "friendly": + if self.options.logic == "friendly": process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), ["MagicMirror"]) process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player), ["Mask"]) - if self.multiworld.map_shuffle[self.player] in ("none", "overworld"): + if self.options.map_shuffle in ("none", "overworld"): process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player), ["Bomb"]) process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player), @@ -185,8 +182,8 @@ def check_foresta(region): process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player), ["DragonClaw", "CaptainCap"]) - if self.multiworld.logic[self.player] == "expert": - if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]: + if self.options.logic == "expert": + if self.options.map_shuffle == "none" and not self.options.crest_shuffle: inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player) connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room) connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player)) @@ -198,14 +195,14 @@ def check_foresta(region): if entrance.connected_region.name in non_dead_end_crest_rooms: entrance.access_rule = lambda state: False - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value] + if self.options.sky_coin_mode == "shattered_sky_coin": + logic_coins = [16, 24, 32, 32, 38][self.options.shattered_sky_coin_quantity.value] self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Fragment", self.player, logic_coins) - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player) - elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"): + elif self.options.sky_coin_mode in ("standard", "start_with"): self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Coin", self.player) @@ -213,24 +210,22 @@ def check_foresta(region): def stage_set_rules(multiworld): # If there's no enemies, there's no repeatable income sources no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest") - if multiworld.enemies_density[player] == "none"] - if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler, - ItemClassification.trap)]) > len([player for player in no_enemies_players if - multiworld.accessibility[player] == "minimal"]) * 3): + if multiworld.worlds[player].options.enemies_density == "none"] + if ( + len([item for item in multiworld.itempool if item.excludable]) > + len([player + for player in no_enemies_players + if multiworld.worlds[player].options.accessibility != "minimal"]) * 3 + ): for player in no_enemies_players: for location in vendor_locations: - if multiworld.accessibility[player] == "locations": + if multiworld.worlds[player].options.accessibility == "full": multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED else: multiworld.get_location(location, player).access_rule = lambda state: False else: - # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing - # advancement items so that useful items can be placed - for player in no_enemies_players: - for location in vendor_locations: - multiworld.get_location(location, player).item_rule = lambda item: not item.advancement - - + raise Exception(f"Not enough filler/trap items for FFMQ players with full and items accessibility. " + f"Add more items or change the 'Enemies Density' option to something besides 'none'") class FFMQLocation(Location): diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index ac3e91370933..3c58487265a6 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -10,7 +10,7 @@ non_dead_end_crest_warps from .Items import item_table, item_groups, create_items, FFMQItem, fillers from .Output import generate_output -from .Options import option_definitions +from .Options import FFMQOptions from .Client import FFMQClient @@ -25,14 +25,25 @@ class FFMQWebWorld(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Multiworld Setup Guide", "A guide to playing Final Fantasy Mystic Quest with Archipelago.", "English", "setup_en.md", "setup/en", ["Alchav"] - )] + ) + + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Artea"] + ) + + tutorials = [setup_en, setup_fr] class FFMQWorld(World): @@ -45,7 +56,8 @@ class FFMQWorld(World): item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None} location_name_to_id = location_table - option_definitions = option_definitions + options_dataclass = FFMQOptions + options: FFMQOptions topology_present = True @@ -67,20 +79,14 @@ def __init__(self, world, player: int): super().__init__(world, player) def generate_early(self): - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - self.multiworld.brown_boxes[self.player].value = 1 - if self.multiworld.enemies_scaling_lower[self.player].value > \ - self.multiworld.enemies_scaling_upper[self.player].value: - (self.multiworld.enemies_scaling_lower[self.player].value, - self.multiworld.enemies_scaling_upper[self.player].value) =\ - (self.multiworld.enemies_scaling_upper[self.player].value, - self.multiworld.enemies_scaling_lower[self.player].value) - if self.multiworld.bosses_scaling_lower[self.player].value > \ - self.multiworld.bosses_scaling_upper[self.player].value: - (self.multiworld.bosses_scaling_lower[self.player].value, - self.multiworld.bosses_scaling_upper[self.player].value) =\ - (self.multiworld.bosses_scaling_upper[self.player].value, - self.multiworld.bosses_scaling_lower[self.player].value) + if self.options.sky_coin_mode == "shattered_sky_coin": + self.options.brown_boxes.value = 1 + if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value: + self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \ + self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value + if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value: + self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \ + self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value @classmethod def stage_generate_early(cls, multiworld): @@ -94,20 +100,20 @@ def stage_generate_early(cls, multiworld): rooms_data = {} for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"): - if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or - world.multiworld.crest_shuffle[world.player]): - if world.multiworld.map_shuffle_seed[world.player].value.isdigit(): - multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value)) - elif world.multiworld.map_shuffle_seed[world.player].value != "random": - multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value)) - + int(world.multiworld.seed)) + if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards + or world.options.companions_locations): + if world.options.map_shuffle_seed.value.isdigit(): + multiworld.random.seed(int(world.options.map_shuffle_seed.value)) + elif world.options.map_shuffle_seed.value != "random": + multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value)) + + int(world.multiworld.seed)) seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() - map_shuffle = multiworld.map_shuffle[world.player].value - crest_shuffle = multiworld.crest_shuffle[world.player].current_key - battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key - companion_shuffle = multiworld.companions_locations[world.player].value - kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key + map_shuffle = world.options.map_shuffle.value + crest_shuffle = world.options.crest_shuffle.current_key + battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key + companion_shuffle = world.options.companions_locations.value + kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}" @@ -175,14 +181,14 @@ def get_filler_item_name(self): def extend_hint_information(self, hint_data): hint_data[self.player] = {} - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"] for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg", "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship", "Subregion Doom Castle"]: region = self.multiworld.get_region(subregion, self.player) for location in region.locations: - if location.address and self.multiworld.map_shuffle[self.player] != "dungeons": + if location.address and self.options.map_shuffle != "dungeons": hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else "")) @@ -202,14 +208,13 @@ def extend_hint_information(self, hint_data): for location in exit_check.connected_region.locations: if location.address: hint = [] - if self.multiworld.map_shuffle[self.player] != "dungeons": + if self.options.map_shuffle != "dungeons": hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else ""))) - if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \ - ("Subregion Mac's Ship", "Subregion Doom Castle"): + if self.options.map_shuffle != "overworld": hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", "Pazuzu's")) - hint = " - ".join(hint) + hint = " - ".join(hint).replace(" - Mac Ship", "") if location.address in hint_data[self.player]: hint_data[self.player][location.address] += f"/{hint}" else: diff --git a/worlds/ffmq/data/entrances.yaml b/worlds/ffmq/data/entrances.yaml deleted file mode 100644 index 1dfef2655c37..000000000000 --- a/worlds/ffmq/data/entrances.yaml +++ /dev/null @@ -1,2450 +0,0 @@ -- name: Doom Castle - Sand Floor - To Sky Door - Sand Floor - id: 0 - area: 7 - coordinates: [24, 19] - teleporter: [0, 0] -- name: Doom Castle - Sand Floor - Main Entrance - Sand Floor - id: 1 - area: 7 - coordinates: [19, 43] - teleporter: [1, 6] -- name: Doom Castle - Aero Room - Aero Room Entrance - id: 2 - area: 7 - coordinates: [27, 39] - teleporter: [1, 0] -- name: Focus Tower B1 - Main Loop - South Entrance - id: 3 - area: 8 - coordinates: [43, 60] - teleporter: [2, 6] -- name: Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall - id: 4 - area: 8 - coordinates: [37, 41] - teleporter: [4, 0] -- name: Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room - id: 5 - area: 8 - coordinates: [59, 35] - teleporter: [5, 0] -- name: Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest - id: 6 - area: 8 - coordinates: [57, 59] - teleporter: [8, 0] -- name: Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door - id: 7 - area: 8 - coordinates: [51, 49] - teleporter: [6, 0] -- name: Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor - id: 8 - area: 8 - coordinates: [51, 45] - teleporter: [7, 0] -- name: Focus Tower 1F - Focus Tower West Entrance - id: 9 - area: 9 - coordinates: [25, 29] - teleporter: [3, 6] -- name: Focus Tower 1F - To Focus Tower 2F - From SandCoin - id: 10 - area: 9 - coordinates: [16, 4] - teleporter: [10, 0] -- name: Focus Tower 1F - To Focus Tower B1 - Main Hall - id: 11 - area: 9 - coordinates: [4, 23] - teleporter: [11, 0] -- name: Focus Tower 1F - To Focus Tower B1 - To Aero Chest - id: 12 - area: 9 - coordinates: [26, 17] - teleporter: [12, 0] -- name: Focus Tower 1F - Sky Door - id: 13 - area: 9 - coordinates: [16, 24] - teleporter: [13, 0] -- name: Focus Tower 1F - To Focus Tower 2F - From RiverCoin - id: 14 - area: 9 - coordinates: [16, 10] - teleporter: [14, 0] -- name: Focus Tower 1F - To Focus Tower B1 - From Sky Door - id: 15 - area: 9 - coordinates: [16, 29] - teleporter: [15, 0] -- name: Focus Tower 2F - Sand Coin Passage - North Entrance - id: 16 - area: 10 - coordinates: [49, 30] - teleporter: [4, 6] -- name: Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin - id: 17 - area: 10 - coordinates: [47, 33] - teleporter: [17, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin - id: 18 - area: 10 - coordinates: [47, 41] - teleporter: [18, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor - id: 19 - area: 10 - coordinates: [38, 40] - teleporter: [20, 0] -- name: Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor - id: 20 - area: 10 - coordinates: [56, 40] - teleporter: [19, 0] -- name: Focus Tower 2F - Venus Chest Room - Pillar Script - id: 21 - area: 10 - coordinates: [48, 53] - teleporter: [13, 8] -- name: Focus Tower 3F - Lower Floor - To Fireburg Entrance - id: 22 - area: 11 - coordinates: [11, 39] - teleporter: [6, 6] -- name: Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar - id: 23 - area: 11 - coordinates: [6, 47] - teleporter: [24, 0] -- name: Focus Tower 3F - Upper Floor - To Aquaria Entrance - id: 24 - area: 11 - coordinates: [21, 38] - teleporter: [5, 6] -- name: Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room - id: 25 - area: 11 - coordinates: [24, 47] - teleporter: [23, 0] -- name: Level Forest - Boulder Script - id: 26 - area: 14 - coordinates: [52, 15] - teleporter: [0, 8] -- name: Level Forest - Rotten Tree Script - id: 27 - area: 14 - coordinates: [47, 6] - teleporter: [2, 8] -- name: Level Forest - Exit Level Forest 1 - id: 28 - area: 14 - coordinates: [46, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 2 - id: 29 - area: 14 - coordinates: [46, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 3 - id: 30 - area: 14 - coordinates: [47, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 4 - id: 31 - area: 14 - coordinates: [47, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 5 - id: 32 - area: 14 - coordinates: [60, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 6 - id: 33 - area: 14 - coordinates: [61, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 7 - id: 34 - area: 14 - coordinates: [46, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 8 - id: 35 - area: 14 - coordinates: [46, 3] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 9 - id: 36 - area: 14 - coordinates: [47, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest A - id: 37 - area: 14 - coordinates: [47, 3] - teleporter: [25, 0] -- name: Foresta - Exit Foresta 1 - id: 38 - area: 15 - coordinates: [10, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 2 - id: 39 - area: 15 - coordinates: [10, 26] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 3 - id: 40 - area: 15 - coordinates: [11, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 4 - id: 41 - area: 15 - coordinates: [11, 26] - teleporter: [31, 0] -- name: Foresta - Old Man House - Front Door - id: 42 - area: 15 - coordinates: [25, 17] - teleporter: [32, 4] -- name: Foresta - Old Man House - Back Door - id: 43 - area: 15 - coordinates: [25, 14] - teleporter: [33, 0] -- name: Foresta - Kaeli's House - id: 44 - area: 15 - coordinates: [7, 21] - teleporter: [0, 5] -- name: Foresta - Rest House - id: 45 - area: 15 - coordinates: [23, 23] - teleporter: [1, 5] -- name: Kaeli's House - Kaeli's House Entrance - id: 46 - area: 16 - coordinates: [11, 20] - teleporter: [86, 3] -- name: Foresta Houses - Old Man's House - Old Man Front Exit - id: 47 - area: 17 - coordinates: [35, 44] - teleporter: [34, 0] -- name: Foresta Houses - Old Man's House - Old Man Back Exit - id: 48 - area: 17 - coordinates: [35, 27] - teleporter: [35, 0] -- name: Foresta - Old Man House - Barrel Tile Script # New, use the focus tower column's script - id: 483 - area: 17 - coordinates: [0x23, 0x1E] - teleporter: [0x0D, 8] -- name: Foresta Houses - Rest House - Bed Script - id: 49 - area: 17 - coordinates: [30, 6] - teleporter: [1, 8] -- name: Foresta Houses - Rest House - Rest House Exit - id: 50 - area: 17 - coordinates: [35, 20] - teleporter: [87, 3] -- name: Foresta Houses - Libra House - Libra House Script - id: 51 - area: 17 - coordinates: [8, 49] - teleporter: [67, 8] -- name: Foresta Houses - Gemini House - Gemini House Script - id: 52 - area: 17 - coordinates: [26, 55] - teleporter: [68, 8] -- name: Foresta Houses - Mobius House - Mobius House Script - id: 53 - area: 17 - coordinates: [14, 33] - teleporter: [69, 8] -- name: Sand Temple - Sand Temple Entrance - id: 54 - area: 18 - coordinates: [56, 27] - teleporter: [36, 0] -- name: Bone Dungeon 1F - Bone Dungeon Entrance - id: 55 - area: 19 - coordinates: [13, 60] - teleporter: [37, 0] -- name: Bone Dungeon 1F - To Bone Dungeon B1 - id: 56 - area: 19 - coordinates: [13, 39] - teleporter: [2, 2] -- name: Bone Dungeon B1 - Waterway - Exit Waterway - id: 57 - area: 20 - coordinates: [27, 39] - teleporter: [3, 2] -- name: Bone Dungeon B1 - Waterway - Tristam's Script - id: 58 - area: 20 - coordinates: [27, 45] - teleporter: [3, 8] -- name: Bone Dungeon B1 - Waterway - To Bone Dungeon 1F - id: 59 - area: 20 - coordinates: [54, 61] - teleporter: [88, 3] -- name: Bone Dungeon B1 - Checker Room - Exit Checker Room - id: 60 - area: 20 - coordinates: [23, 40] - teleporter: [4, 2] -- name: Bone Dungeon B1 - Checker Room - To Waterway - id: 61 - area: 20 - coordinates: [39, 49] - teleporter: [89, 3] -- name: Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room - id: 62 - area: 20 - coordinates: [5, 33] - teleporter: [91, 3] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage - id: 63 - area: 21 - coordinates: [19, 13] - teleporter: [5, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room - id: 64 - area: 21 - coordinates: [29, 15] - teleporter: [6, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Checker Room - id: 65 - area: 21 - coordinates: [8, 25] - teleporter: [90, 3] -- name: Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room - id: 66 - area: 21 - coordinates: [59, 12] - teleporter: [93, 3] -- name: Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room - id: 67 - area: 21 - coordinates: [59, 28] - teleporter: [94, 3] -- name: Bonne Dungeon B2 - Two Skulls Room - To Box Room - id: 68 - area: 21 - coordinates: [53, 7] - teleporter: [7, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Quake Room - id: 69 - area: 21 - coordinates: [41, 3] - teleporter: [8, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Boss Room - id: 70 - area: 21 - coordinates: [47, 57] - teleporter: [9, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room - id: 71 - area: 21 - coordinates: [54, 23] - teleporter: [92, 3] -- name: Bone Dungeon B2 - Boss Room - Flamerus Rex Script - id: 72 - area: 22 - coordinates: [29, 19] - teleporter: [4, 8] -- name: Bone Dungeon B2 - Boss Room - Tristam Leave Script - id: 73 - area: 22 - coordinates: [29, 23] - teleporter: [75, 8] -- name: Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room - id: 74 - area: 22 - coordinates: [30, 27] - teleporter: [95, 3] -- name: Libra Temple - Entrance - id: 75 - area: 23 - coordinates: [10, 15] - teleporter: [13, 6] -- name: Libra Temple - Libra Tile Script - id: 76 - area: 23 - coordinates: [9, 8] - teleporter: [59, 8] -- name: Aquaria Winter - Winter Entrance 1 - id: 77 - area: 24 - coordinates: [25, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 2 - id: 78 - area: 24 - coordinates: [25, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 3 - id: 79 - area: 24 - coordinates: [26, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 4 - id: 80 - area: 24 - coordinates: [26, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Phoebe's House Entrance Script #Modified to not be a script - id: 81 - area: 24 - coordinates: [8, 19] - teleporter: [10, 5] # original value [5, 8] -- name: Aquaria Winter - Winter Vendor House Entrance - id: 82 - area: 24 - coordinates: [8, 5] - teleporter: [44, 4] -- name: Aquaria Winter - Winter INN Entrance - id: 83 - area: 24 - coordinates: [26, 17] - teleporter: [11, 5] -- name: Aquaria Summer - Summer Entrance 1 - id: 84 - area: 25 - coordinates: [57, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 2 - id: 85 - area: 25 - coordinates: [57, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 3 - id: 86 - area: 25 - coordinates: [58, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 4 - id: 87 - area: 25 - coordinates: [58, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Phoebe's House Entrance - id: 88 - area: 25 - coordinates: [40, 19] - teleporter: [10, 5] -- name: Aquaria Summer - Spencer's Place Entrance Top - id: 89 - area: 25 - coordinates: [40, 16] - teleporter: [42, 0] -- name: Aquaria Summer - Spencer's Place Entrance Side - id: 90 - area: 25 - coordinates: [41, 18] - teleporter: [43, 0] -- name: Aquaria Summer - Summer Vendor House Entrance - id: 91 - area: 25 - coordinates: [40, 5] - teleporter: [44, 4] -- name: Aquaria Summer - Summer INN Entrance - id: 92 - area: 25 - coordinates: [58, 17] - teleporter: [11, 5] -- name: Phoebe's House - Entrance # Change to a script, same as vendor house - id: 93 - area: 26 - coordinates: [29, 14] - teleporter: [5, 8] # Original Value [11,3] -- name: Aquaria Vendor House - Vendor House Entrance's Script - id: 94 - area: 27 - coordinates: [7, 10] - teleporter: [40, 8] -- name: Aquaria Vendor House - Vendor House Stairs - id: 95 - area: 27 - coordinates: [1, 4] - teleporter: [47, 0] -- name: Aquaria Gemini Room - Gemini Script - id: 96 - area: 27 - coordinates: [2, 40] - teleporter: [72, 8] -- name: Aquaria Gemini Room - Gemini Room Stairs - id: 97 - area: 27 - coordinates: [4, 39] - teleporter: [48, 0] -- name: Aquaria INN - Aquaria INN entrance # Change to a script, same as vendor house - id: 98 - area: 27 - coordinates: [51, 46] - teleporter: [75, 8] # Original value [48,3] -- name: Wintry Cave 1F - Main Entrance - id: 99 - area: 28 - coordinates: [50, 58] - teleporter: [49, 0] -- name: Wintry Cave 1F - To 3F Top - id: 100 - area: 28 - coordinates: [40, 25] - teleporter: [14, 2] -- name: Wintry Cave 1F - To 2F - id: 101 - area: 28 - coordinates: [10, 43] - teleporter: [15, 2] -- name: Wintry Cave 1F - Phoebe's Script - id: 102 - area: 28 - coordinates: [44, 37] - teleporter: [6, 8] -- name: Wintry Cave 2F - To 3F Bottom - id: 103 - area: 29 - coordinates: [58, 5] - teleporter: [50, 0] -- name: Wintry Cave 2F - To 1F - id: 104 - area: 29 - coordinates: [38, 18] - teleporter: [97, 3] -- name: Wintry Cave 3F Top - Exit from 3F Top - id: 105 - area: 30 - coordinates: [24, 6] - teleporter: [96, 3] -- name: Wintry Cave 3F Bottom - Exit to 2F - id: 106 - area: 31 - coordinates: [4, 29] - teleporter: [51, 0] -- name: Life Temple - Entrance - id: 107 - area: 32 - coordinates: [9, 60] - teleporter: [14, 6] -- name: Life Temple - Libra Tile Script - id: 108 - area: 32 - coordinates: [3, 55] - teleporter: [60, 8] -- name: Life Temple - Mysterious Man Script - id: 109 - area: 32 - coordinates: [9, 44] - teleporter: [78, 8] -- name: Fall Basin - Back Exit Script - id: 110 - area: 33 - coordinates: [17, 5] - teleporter: [9, 0] # Remove script [42, 8] for overworld teleport (but not main exit) -- name: Fall Basin - Main Exit - id: 111 - area: 33 - coordinates: [15, 26] - teleporter: [53, 0] -- name: Fall Basin - Phoebe's Script - id: 112 - area: 33 - coordinates: [17, 6] - teleporter: [9, 8] -- name: Ice Pyramid B1 Taunt Room - To Climbing Wall Room - id: 113 - area: 34 - coordinates: [43, 6] - teleporter: [55, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 1 - id: 114 - area: 35 - coordinates: [18, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 2 - id: 115 - area: 35 - coordinates: [19, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room - id: 116 - area: 35 - coordinates: [3, 27] - teleporter: [57, 0] -- name: Ice Pyramid 1F Maze - West Center Stairs to 2F West Room - id: 117 - area: 35 - coordinates: [11, 15] - teleporter: [58, 0] -- name: Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room - id: 118 - area: 35 - coordinates: [25, 16] - teleporter: [59, 0] -- name: Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room - id: 119 - area: 35 - coordinates: [31, 1] - teleporter: [60, 0] -- name: Ice Pyramid 1F Maze - East Stairs to 2F North Corridor - id: 120 - area: 35 - coordinates: [34, 9] - teleporter: [61, 0] -- name: Ice Pyramid 1F Maze - Statue's Script - id: 121 - area: 35 - coordinates: [21, 32] - teleporter: [77, 8] -- name: Ice Pyramid 2F South Tiled Room - To 1F - id: 122 - area: 36 - coordinates: [4, 26] - teleporter: [62, 0] -- name: Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room - id: 123 - area: 36 - coordinates: [22, 17] - teleporter: [67, 0] -- name: Ice Pyramid 2F West Room - To 1F - id: 124 - area: 36 - coordinates: [9, 10] - teleporter: [63, 0] -- name: Ice Pyramid 2F Center Room - To 1F - id: 125 - area: 36 - coordinates: [22, 14] - teleporter: [64, 0] -- name: Ice Pyramid 2F Small North Room - To 1F - id: 126 - area: 36 - coordinates: [26, 4] - teleporter: [65, 0] -- name: Ice Pyramid 2F North Corridor - To 1F - id: 127 - area: 36 - coordinates: [32, 8] - teleporter: [66, 0] -- name: Ice Pyramid 2F North Corridor - To 3F Main Loop - id: 128 - area: 36 - coordinates: [12, 7] - teleporter: [68, 0] -- name: Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room - id: 129 - area: 37 - coordinates: [24, 54] - teleporter: [69, 0] -- name: Ice Pyramid 3F Main Loop - To 2F Corridor - id: 130 - area: 37 - coordinates: [16, 45] - teleporter: [70, 0] -- name: Ice Pyramid 3F Main Loop - To 4F - id: 131 - area: 37 - coordinates: [19, 43] - teleporter: [71, 0] -- name: Ice Pyramid 4F Treasure Room - To 3F Main Loop - id: 132 - area: 38 - coordinates: [52, 5] - teleporter: [72, 0] -- name: Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room - id: 133 - area: 38 - coordinates: [62, 19] - teleporter: [73, 0] -- name: Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room - id: 134 - area: 39 - coordinates: [54, 63] - teleporter: [74, 0] -- name: Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate - id: 135 - area: 39 - coordinates: [47, 54] - teleporter: [77, 8] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room - id: 136 - area: 39 - coordinates: [39, 43] - teleporter: [75, 0] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room - id: 137 - area: 39 - coordinates: [39, 60] - teleporter: [76, 0] -- name: Ice Pyramid - Duplicate Ice Golem Room # not used? - id: 138 - area: 40 - coordinates: [44, 43] - teleporter: [77, 0] -- name: Ice Pyramid Climbing Wall Room - To Taunt Room - id: 139 - area: 41 - coordinates: [4, 59] - teleporter: [78, 0] -- name: Ice Pyramid Climbing Wall Room - To 5F Stairs - id: 140 - area: 41 - coordinates: [4, 45] - teleporter: [79, 0] -- name: Ice Pyramid Ice Golem Room - To 5F Stairs - id: 141 - area: 42 - coordinates: [44, 43] - teleporter: [80, 0] -- name: Ice Pyramid Ice Golem Room - Ice Golem Script - id: 142 - area: 42 - coordinates: [53, 32] - teleporter: [10, 8] -- name: Spencer Waterfall - To Spencer Cave - id: 143 - area: 43 - coordinates: [48, 57] - teleporter: [81, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 1 - id: 144 - area: 43 - coordinates: [40, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 2 - id: 145 - area: 43 - coordinates: [40, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 3 - id: 146 - area: 43 - coordinates: [41, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 4 - id: 147 - area: 43 - coordinates: [41, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 1 - id: 148 - area: 43 - coordinates: [46, 8] - teleporter: [83, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 2 - id: 149 - area: 43 - coordinates: [47, 8] - teleporter: [83, 0] -- name: Spencer Cave Normal Main - To Waterfall - id: 150 - area: 44 - coordinates: [14, 39] - teleporter: [85, 0] -- name: Spencer Cave Normal From Overworld - Exit to Overworld - id: 151 - area: 44 - coordinates: [15, 57] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Exit to Overworld - id: 152 - area: 45 - coordinates: [40, 29] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Libra Teleporter Start Script - id: 153 - area: 45 - coordinates: [28, 21] - teleporter: [33, 8] -- name: Spencer Cave Unplug - Libra Teleporter End Script - id: 154 - area: 45 - coordinates: [46, 4] - teleporter: [34, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Chest Script - id: 155 - area: 45 - coordinates: [21, 9] - teleporter: [35, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Start Script - id: 156 - area: 45 - coordinates: [29, 28] - teleporter: [36, 8] -- name: Wintry Temple Outer Room - Main Entrance - id: 157 - area: 46 - coordinates: [8, 31] - teleporter: [15, 6] -- name: Wintry Temple Inner Room - Gemini Tile to Sealed temple - id: 158 - area: 46 - coordinates: [9, 24] - teleporter: [62, 8] -- name: Fireburg - To Overworld - id: 159 - area: 47 - coordinates: [4, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 160 - area: 47 - coordinates: [5, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 161 - area: 47 - coordinates: [28, 15] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 162 - area: 47 - coordinates: [27, 15] - teleporter: [9, 6] -- name: Fireburg - Vendor House - id: 163 - area: 47 - coordinates: [10, 24] - teleporter: [91, 0] -- name: Fireburg - Reuben House - id: 164 - area: 47 - coordinates: [14, 6] - teleporter: [98, 8] # Script for reuben, original value [16, 2] -- name: Fireburg - Hotel - id: 165 - area: 47 - coordinates: [20, 8] - teleporter: [96, 8] # It's a script now for tristam, original value [17, 2] -- name: Fireburg - GrenadeMan House Script - id: 166 - area: 47 - coordinates: [12, 18] - teleporter: [11, 8] -- name: Reuben House - Main Entrance - id: 167 - area: 48 - coordinates: [33, 46] - teleporter: [98, 3] -- name: GrenadeMan House - Entrance Script - id: 168 - area: 49 - coordinates: [55, 60] - teleporter: [9, 8] -- name: GrenadeMan House - To Mobius Crest Room - id: 169 - area: 49 - coordinates: [57, 52] - teleporter: [93, 0] -- name: GrenadeMan Mobius Room - Stairs to House - id: 170 - area: 49 - coordinates: [39, 26] - teleporter: [94, 0] -- name: GrenadeMan Mobius Room - Mobius Teleporter Script - id: 171 - area: 49 - coordinates: [39, 23] - teleporter: [54, 8] -- name: Fireburg Vendor House - Entrance Script # No use to be a script - id: 172 - area: 49 - coordinates: [7, 10] - teleporter: [95, 0] # Original value [39, 8] -- name: Fireburg Vendor House - Stairs to Gemini Room - id: 173 - area: 49 - coordinates: [1, 4] - teleporter: [96, 0] -- name: Fireburg Gemini Room - Stairs to Vendor House - id: 174 - area: 49 - coordinates: [4, 39] - teleporter: [97, 0] -- name: Fireburg Gemini Room - Gemini Teleporter Script - id: 175 - area: 49 - coordinates: [2, 40] - teleporter: [45, 8] -- name: Fireburg Hotel Lobby - Stairs to beds - id: 176 - area: 49 - coordinates: [4, 50] - teleporter: [213, 0] -- name: Fireburg Hotel Lobby - Entrance - id: 177 - area: 49 - coordinates: [17, 56] - teleporter: [99, 3] -- name: Fireburg Hotel Beds - Stairs to Hotel Lobby - id: 178 - area: 49 - coordinates: [45, 59] - teleporter: [214, 0] -- name: Mine Exterior - Main Entrance - id: 179 - area: 50 - coordinates: [5, 28] - teleporter: [98, 0] -- name: Mine Exterior - To Cliff - id: 180 - area: 50 - coordinates: [58, 29] - teleporter: [99, 0] -- name: Mine Exterior - To Parallel Room - id: 181 - area: 50 - coordinates: [8, 7] - teleporter: [20, 2] -- name: Mine Exterior - To Crescent Room - id: 182 - area: 50 - coordinates: [26, 15] - teleporter: [21, 2] -- name: Mine Exterior - To Climbing Room - id: 183 - area: 50 - coordinates: [21, 35] - teleporter: [22, 2] -- name: Mine Exterior - Jinn Fight Script - id: 184 - area: 50 - coordinates: [58, 31] - teleporter: [74, 8] -- name: Mine Parallel Room - To Mine Exterior - id: 185 - area: 51 - coordinates: [7, 60] - teleporter: [100, 3] -- name: Mine Crescent Room - To Mine Exterior - id: 186 - area: 51 - coordinates: [22, 61] - teleporter: [101, 3] -- name: Mine Climbing Room - To Mine Exterior - id: 187 - area: 51 - coordinates: [56, 21] - teleporter: [102, 3] -- name: Mine Cliff - Entrance - id: 188 - area: 52 - coordinates: [9, 5] - teleporter: [100, 0] -- name: Mine Cliff - Reuben Grenade Script - id: 189 - area: 52 - coordinates: [15, 7] - teleporter: [12, 8] -- name: Sealed Temple - To Overworld - id: 190 - area: 53 - coordinates: [58, 43] - teleporter: [16, 6] -- name: Sealed Temple - Gemini Tile Script - id: 191 - area: 53 - coordinates: [56, 38] - teleporter: [63, 8] -- name: Volcano Base - Main Entrance 1 - id: 192 - area: 54 - coordinates: [23, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 2 - id: 193 - area: 54 - coordinates: [23, 26] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 3 - id: 194 - area: 54 - coordinates: [24, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 4 - id: 195 - area: 54 - coordinates: [24, 26] - teleporter: [103, 0] -- name: Volcano Base - Left Stairs Script - id: 196 - area: 54 - coordinates: [20, 5] - teleporter: [31, 8] -- name: Volcano Base - Right Stairs Script - id: 197 - area: 54 - coordinates: [32, 5] - teleporter: [30, 8] -- name: Volcano Top Right - Top Exit - id: 198 - area: 55 - coordinates: [44, 8] - teleporter: [9, 0] # Original value [103, 0] changed to volcano escape so floor shuffling doesn't pick it up -- name: Volcano Top Left - To Right-Left Path Script - id: 199 - area: 55 - coordinates: [40, 24] - teleporter: [26, 8] -- name: Volcano Top Right - To Left-Right Path Script - id: 200 - area: 55 - coordinates: [52, 24] - teleporter: [79, 8] # Original Value [26, 8] -- name: Volcano Right Path - To Volcano Base Script - id: 201 - area: 56 - coordinates: [48, 42] - teleporter: [15, 8] # Original Value [27, 8] -- name: Volcano Left Path - To Volcano Cross Left-Right - id: 202 - area: 56 - coordinates: [40, 31] - teleporter: [25, 2] -- name: Volcano Left Path - To Volcano Cross Right-Left - id: 203 - area: 56 - coordinates: [52, 29] - teleporter: [26, 2] -- name: Volcano Left Path - To Volcano Base Script - id: 204 - area: 56 - coordinates: [36, 42] - teleporter: [27, 8] -- name: Volcano Cross Left-Right - To Volcano Left Path - id: 205 - area: 56 - coordinates: [10, 42] - teleporter: [103, 3] -- name: Volcano Cross Left-Right - To Volcano Top Right Script - id: 206 - area: 56 - coordinates: [16, 24] - teleporter: [29, 8] -- name: Volcano Cross Right-Left - To Volcano Top Left Script - id: 207 - area: 56 - coordinates: [8, 22] - teleporter: [28, 8] -- name: Volcano Cross Right-Left - To Volcano Left Path - id: 208 - area: 56 - coordinates: [16, 42] - teleporter: [104, 3] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 1 - id: 209 - area: 57 - coordinates: [32, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 2 - id: 210 - area: 57 - coordinates: [33, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - To Three Steps Room - id: 211 - area: 57 - coordinates: [14, 5] - teleporter: [105, 0] -- name: Lava Dome Inner Ring Main Loop - To Life Chest Room Lower - id: 212 - area: 57 - coordinates: [40, 17] - teleporter: [106, 0] -- name: Lava Dome Inner Ring Main Loop - To Big Jump Room Left - id: 213 - area: 57 - coordinates: [8, 11] - teleporter: [108, 0] -- name: Lava Dome Inner Ring Main Loop - To Split Corridor Room - id: 214 - area: 57 - coordinates: [11, 19] - teleporter: [111, 0] -- name: Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher - id: 215 - area: 57 - coordinates: [32, 11] - teleporter: [107, 0] -- name: Lava Dome Inner Ring Plate Ledge - To Plate Corridor - id: 216 - area: 57 - coordinates: [12, 23] - teleporter: [109, 0] -- name: Lava Dome Inner Ring Plate Ledge - Plate Script - id: 217 - area: 57 - coordinates: [5, 23] - teleporter: [47, 8] -- name: Lava Dome Inner Ring Upper Ledges - To Pointless Room - id: 218 - area: 57 - coordinates: [0, 9] - teleporter: [110, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room - id: 219 - area: 57 - coordinates: [0, 15] - teleporter: [112, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor - id: 220 - area: 57 - coordinates: [54, 5] - teleporter: [113, 0] -- name: Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II - id: 221 - area: 57 - coordinates: [54, 21] - teleporter: [114, 0] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1 - id: 222 - area: 57 - coordinates: [62, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2 - id: 223 - area: 57 - coordinates: [63, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3 - id: 224 - area: 57 - coordinates: [62, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4 - id: 225 - area: 57 - coordinates: [63, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor - id: 226 - area: 57 - coordinates: [50, 25] - teleporter: [115, 0] -- name: Lava Dome Jump Maze II - Lower Right Entrance - id: 227 - area: 58 - coordinates: [55, 28] - teleporter: [116, 0] -- name: Lava Dome Jump Maze II - Upper Entrance - id: 228 - area: 58 - coordinates: [35, 3] - teleporter: [119, 0] -- name: Lava Dome Jump Maze II - Lower Left Entrance - id: 229 - area: 58 - coordinates: [34, 27] - teleporter: [120, 0] -- name: Lava Dome Up-Down Corridor - Upper Entrance - id: 230 - area: 58 - coordinates: [29, 8] - teleporter: [117, 0] -- name: Lava Dome Up-Down Corridor - Lower Entrance - id: 231 - area: 58 - coordinates: [28, 25] - teleporter: [118, 0] -- name: Lava Dome Jump Maze I - South Entrance - id: 232 - area: 59 - coordinates: [20, 27] - teleporter: [121, 0] -- name: Lava Dome Jump Maze I - North Entrance - id: 233 - area: 59 - coordinates: [7, 3] - teleporter: [122, 0] -- name: Lava Dome Pointless Room - Entrance - id: 234 - area: 60 - coordinates: [2, 7] - teleporter: [123, 0] -- name: Lava Dome Pointless Room - Visit Quest Script 1 - id: 490 - area: 60 - coordinates: [4, 4] - teleporter: [99, 8] -- name: Lava Dome Pointless Room - Visit Quest Script 2 - id: 491 - area: 60 - coordinates: [4, 5] - teleporter: [99, 8] -- name: Lava Dome Lower Moon Helm Room - Left Entrance - id: 235 - area: 60 - coordinates: [2, 19] - teleporter: [124, 0] -- name: Lava Dome Lower Moon Helm Room - Right Entrance - id: 236 - area: 60 - coordinates: [11, 21] - teleporter: [125, 0] -- name: Lava Dome Moon Helm Room - Entrance - id: 237 - area: 60 - coordinates: [15, 23] - teleporter: [126, 0] -- name: Lava Dome Three Jumps Room - To Main Loop - id: 238 - area: 61 - coordinates: [58, 15] - teleporter: [127, 0] -- name: Lava Dome Life Chest Room - Lower South Entrance - id: 239 - area: 61 - coordinates: [38, 27] - teleporter: [128, 0] -- name: Lava Dome Life Chest Room - Upper South Entrance - id: 240 - area: 61 - coordinates: [28, 23] - teleporter: [129, 0] -- name: Lava Dome Big Jump Room - Left Entrance - id: 241 - area: 62 - coordinates: [42, 51] - teleporter: [133, 0] -- name: Lava Dome Big Jump Room - North Entrance - id: 242 - area: 62 - coordinates: [30, 29] - teleporter: [131, 0] -- name: Lava Dome Big Jump Room - Lower Right Stairs - id: 243 - area: 62 - coordinates: [61, 59] - teleporter: [132, 0] -- name: Lava Dome Split Corridor - Upper Stairs - id: 244 - area: 62 - coordinates: [30, 43] - teleporter: [130, 0] -- name: Lava Dome Split Corridor - Lower Stairs - id: 245 - area: 62 - coordinates: [36, 61] - teleporter: [134, 0] -- name: Lava Dome Plate Corridor - Right Entrance - id: 246 - area: 63 - coordinates: [19, 29] - teleporter: [135, 0] -- name: Lava Dome Plate Corridor - Left Entrance - id: 247 - area: 63 - coordinates: [60, 21] - teleporter: [137, 0] -- name: Lava Dome Four Boxes Stairs - Upper Entrance - id: 248 - area: 63 - coordinates: [22, 3] - teleporter: [136, 0] -- name: Lava Dome Four Boxes Stairs - Lower Entrance - id: 249 - area: 63 - coordinates: [22, 17] - teleporter: [16, 0] -- name: Lava Dome Hydra Room - South Entrance - id: 250 - area: 64 - coordinates: [14, 59] - teleporter: [105, 3] -- name: Lava Dome Hydra Room - North Exit - id: 251 - area: 64 - coordinates: [25, 31] - teleporter: [138, 0] -- name: Lava Dome Hydra Room - Hydra Script - id: 252 - area: 64 - coordinates: [14, 36] - teleporter: [14, 8] -- name: Lava Dome Escape Corridor - South Entrance - id: 253 - area: 65 - coordinates: [22, 17] - teleporter: [139, 0] -- name: Lava Dome Escape Corridor - North Entrance - id: 254 - area: 65 - coordinates: [22, 3] - teleporter: [9, 0] -- name: Rope Bridge - West Entrance 1 - id: 255 - area: 66 - coordinates: [3, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 2 - id: 256 - area: 66 - coordinates: [3, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 3 - id: 257 - area: 66 - coordinates: [3, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 4 - id: 258 - area: 66 - coordinates: [3, 13] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 5 - id: 259 - area: 66 - coordinates: [4, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 6 - id: 260 - area: 66 - coordinates: [4, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 7 - id: 261 - area: 66 - coordinates: [4, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 8 - id: 262 - area: 66 - coordinates: [4, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 1 - id: 263 - area: 66 - coordinates: [59, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 2 - id: 264 - area: 66 - coordinates: [59, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 3 - id: 265 - area: 66 - coordinates: [59, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 4 - id: 266 - area: 66 - coordinates: [59, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 5 - id: 267 - area: 66 - coordinates: [60, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 6 - id: 268 - area: 66 - coordinates: [60, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 7 - id: 269 - area: 66 - coordinates: [60, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 8 - id: 270 - area: 66 - coordinates: [60, 13] - teleporter: [140, 0] -- name: Rope Bridge - Reuben Fall Script - id: 271 - area: 66 - coordinates: [13, 12] - teleporter: [15, 8] -- name: Alive Forest - West Entrance 1 - id: 272 - area: 67 - coordinates: [8, 13] - teleporter: [142, 0] -- name: Alive Forest - West Entrance 2 - id: 273 - area: 67 - coordinates: [9, 13] - teleporter: [142, 0] -- name: Alive Forest - Giant Tree Entrance - id: 274 - area: 67 - coordinates: [42, 42] - teleporter: [143, 0] -- name: Alive Forest - Libra Teleporter Script - id: 275 - area: 67 - coordinates: [8, 52] - teleporter: [64, 8] -- name: Alive Forest - Gemini Teleporter Script - id: 276 - area: 67 - coordinates: [57, 49] - teleporter: [65, 8] -- name: Alive Forest - Mobius Teleporter Script - id: 277 - area: 67 - coordinates: [24, 10] - teleporter: [66, 8] -- name: Giant Tree 1F - Entrance Script 1 - id: 278 - area: 68 - coordinates: [18, 31] - teleporter: [56, 1] # The script is restored if no map shuffling [49, 8] -- name: Giant Tree 1F - Entrance Script 2 - id: 279 - area: 68 - coordinates: [19, 31] - teleporter: [56, 1] # Same [49, 8] -- name: Giant Tree 1F - North Entrance To 2F - id: 280 - area: 68 - coordinates: [16, 1] - teleporter: [144, 0] -- name: Giant Tree 2F Main Lobby - North Entrance to 1F - id: 281 - area: 69 - coordinates: [44, 33] - teleporter: [145, 0] -- name: Giant Tree 2F Main Lobby - Central Entrance to 3F - id: 282 - area: 69 - coordinates: [42, 47] - teleporter: [146, 0] -- name: Giant Tree 2F Main Lobby - West Entrance to Mushroom Room - id: 283 - area: 69 - coordinates: [58, 49] - teleporter: [149, 0] -- name: Giant Tree 2F West Ledge - To 3F Northwest Ledge - id: 284 - area: 69 - coordinates: [34, 37] - teleporter: [147, 0] -- name: Giant Tree 2F Fall From Vine Script - id: 482 - area: 69 - coordinates: [0x2E, 0x33] - teleporter: [76, 8] -- name: Giant Tree Meteor Chest Room - To 2F Mushroom Room - id: 285 - area: 69 - coordinates: [58, 44] - teleporter: [148, 0] -- name: Giant Tree 2F Mushroom Room - Entrance - id: 286 - area: 70 - coordinates: [55, 18] - teleporter: [150, 0] -- name: Giant Tree 2F Mushroom Room - North Face to Meteor - id: 287 - area: 70 - coordinates: [56, 7] - teleporter: [151, 0] -- name: Giant Tree 3F Central Room - Central Entrance to 2F - id: 288 - area: 71 - coordinates: [46, 53] - teleporter: [152, 0] -- name: Giant Tree 3F Central Room - East Entrance to Worm Room - id: 289 - area: 71 - coordinates: [58, 39] - teleporter: [153, 0] -- name: Giant Tree 3F Lower Corridor - Entrance from Worm Room - id: 290 - area: 71 - coordinates: [45, 39] - teleporter: [154, 0] -- name: Giant Tree 3F West Platform - Lower Entrance - id: 291 - area: 71 - coordinates: [33, 43] - teleporter: [155, 0] -- name: Giant Tree 3F West Platform - Top Entrance - id: 292 - area: 71 - coordinates: [52, 25] - teleporter: [156, 0] -- name: Giant Tree Worm Room - East Entrance - id: 293 - area: 72 - coordinates: [20, 58] - teleporter: [157, 0] -- name: Giant Tree Worm Room - West Entrance - id: 294 - area: 72 - coordinates: [6, 56] - teleporter: [158, 0] -- name: Giant Tree 4F Lower Floor - Entrance - id: 295 - area: 73 - coordinates: [20, 7] - teleporter: [159, 0] -- name: Giant Tree 4F Lower Floor - Lower West Mouth - id: 296 - area: 73 - coordinates: [8, 23] - teleporter: [160, 0] -- name: Giant Tree 4F Lower Floor - Lower Central Mouth - id: 297 - area: 73 - coordinates: [14, 25] - teleporter: [161, 0] -- name: Giant Tree 4F Lower Floor - Lower East Mouth - id: 298 - area: 73 - coordinates: [20, 25] - teleporter: [162, 0] -- name: Giant Tree 4F Upper Floor - Upper West Mouth - id: 299 - area: 73 - coordinates: [8, 19] - teleporter: [163, 0] -- name: Giant Tree 4F Upper Floor - Upper Central Mouth - id: 300 - area: 73 - coordinates: [12, 17] - teleporter: [164, 0] -- name: Giant Tree 4F Slime Room - Exit - id: 301 - area: 74 - coordinates: [47, 10] - teleporter: [165, 0] -- name: Giant Tree 4F Slime Room - West Entrance - id: 302 - area: 74 - coordinates: [45, 24] - teleporter: [166, 0] -- name: Giant Tree 4F Slime Room - Central Entrance - id: 303 - area: 74 - coordinates: [50, 24] - teleporter: [167, 0] -- name: Giant Tree 4F Slime Room - East Entrance - id: 304 - area: 74 - coordinates: [57, 28] - teleporter: [168, 0] -- name: Giant Tree 5F - Entrance - id: 305 - area: 75 - coordinates: [14, 51] - teleporter: [169, 0] -- name: Giant Tree 5F - Giant Tree Face # Unused - id: 306 - area: 75 - coordinates: [14, 37] - teleporter: [170, 0] -- name: Kaidge Temple - Entrance - id: 307 - area: 77 - coordinates: [44, 63] - teleporter: [18, 6] -- name: Kaidge Temple - Mobius Teleporter Script - id: 308 - area: 77 - coordinates: [35, 57] - teleporter: [71, 8] -- name: Windhole Temple - Entrance - id: 309 - area: 78 - coordinates: [10, 29] - teleporter: [173, 0] -- name: Mount Gale - Entrance 1 - id: 310 - area: 79 - coordinates: [1, 45] - teleporter: [174, 0] -- name: Mount Gale - Entrance 2 - id: 311 - area: 79 - coordinates: [2, 45] - teleporter: [174, 0] -- name: Mount Gale - Visit Quest - id: 494 - area: 79 - coordinates: [44, 7] - teleporter: [101, 8] -- name: Windia - Main Entrance 1 - id: 312 - area: 80 - coordinates: [12, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 2 - id: 313 - area: 80 - coordinates: [13, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 3 - id: 314 - area: 80 - coordinates: [14, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 4 - id: 315 - area: 80 - coordinates: [15, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 5 - id: 316 - area: 80 - coordinates: [12, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 6 - id: 317 - area: 80 - coordinates: [13, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 7 - id: 318 - area: 80 - coordinates: [14, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 8 - id: 319 - area: 80 - coordinates: [15, 41] - teleporter: [10, 6] -- name: Windia - Otto's House - id: 320 - area: 80 - coordinates: [21, 39] - teleporter: [30, 5] -- name: Windia - INN's Script # Change to teleporter / Change back to script! - id: 321 - area: 80 - coordinates: [18, 34] - teleporter: [97, 8] # Original value [79, 8] > [31, 2] -- name: Windia - Vendor House - id: 322 - area: 80 - coordinates: [8, 36] - teleporter: [32, 5] -- name: Windia - Kid House - id: 323 - area: 80 - coordinates: [7, 23] - teleporter: [176, 4] -- name: Windia - Old People House - id: 324 - area: 80 - coordinates: [19, 21] - teleporter: [177, 4] -- name: Windia - Rainbow Bridge Script - id: 325 - area: 80 - coordinates: [21, 9] - teleporter: [10, 6] # Change to entrance, usually a script [41, 8] -- name: Otto's House - Attic Stairs - id: 326 - area: 81 - coordinates: [2, 19] - teleporter: [33, 2] -- name: Otto's House - Entrance - id: 327 - area: 81 - coordinates: [9, 30] - teleporter: [106, 3] -- name: Otto's Attic - Stairs - id: 328 - area: 81 - coordinates: [26, 23] - teleporter: [107, 3] -- name: Windia Kid House - Entrance Script # Change to teleporter - id: 329 - area: 82 - coordinates: [7, 10] - teleporter: [178, 0] # Original value [38, 8] -- name: Windia Kid House - Basement Stairs - id: 330 - area: 82 - coordinates: [1, 4] - teleporter: [180, 0] -- name: Windia Old People House - Entrance - id: 331 - area: 82 - coordinates: [55, 12] - teleporter: [179, 0] -- name: Windia Old People House - Basement Stairs - id: 332 - area: 82 - coordinates: [60, 5] - teleporter: [181, 0] -- name: Windia Kid House Basement - Stairs - id: 333 - area: 82 - coordinates: [43, 8] - teleporter: [182, 0] -- name: Windia Kid House Basement - Mobius Teleporter - id: 334 - area: 82 - coordinates: [41, 9] - teleporter: [44, 8] -- name: Windia Old People House Basement - Stairs - id: 335 - area: 82 - coordinates: [39, 26] - teleporter: [183, 0] -- name: Windia Old People House Basement - Mobius Teleporter Script - id: 336 - area: 82 - coordinates: [39, 23] - teleporter: [43, 8] -- name: Windia Inn Lobby - Stairs to Beds - id: 337 - area: 82 - coordinates: [45, 24] - teleporter: [102, 8] # Changed to script, original value [215, 0] -- name: Windia Inn Lobby - Exit - id: 338 - area: 82 - coordinates: [53, 30] - teleporter: [135, 3] -- name: Windia Inn Beds - Stairs to Lobby - id: 339 - area: 82 - coordinates: [33, 59] - teleporter: [216, 0] -- name: Windia Vendor House - Entrance - id: 340 - area: 82 - coordinates: [29, 14] - teleporter: [108, 3] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 1 - id: 341 - area: 83 - coordinates: [47, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 2 - id: 342 - area: 83 - coordinates: [47, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 3 - id: 343 - area: 83 - coordinates: [48, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 4 - id: 344 - area: 83 - coordinates: [48, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - East Entrance - id: 345 - area: 83 - coordinates: [55, 12] - teleporter: [185, 0] -- name: Pazuzu Tower 1F Main Lobby - South Stairs - id: 346 - area: 83 - coordinates: [51, 25] - teleporter: [186, 0] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 1 - id: 347 - area: 83 - coordinates: [47, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 2 - id: 348 - area: 83 - coordinates: [48, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Boxes Room - West Stairs - id: 349 - area: 83 - coordinates: [38, 17] - teleporter: [187, 0] -- name: Pazuzu 2F - West Upper Stairs - id: 350 - area: 84 - coordinates: [7, 11] - teleporter: [188, 0] -- name: Pazuzu 2F - South Stairs - id: 351 - area: 84 - coordinates: [20, 24] - teleporter: [189, 0] -- name: Pazuzu 2F - West Lower Stairs - id: 352 - area: 84 - coordinates: [6, 17] - teleporter: [190, 0] -- name: Pazuzu 2F - Central Stairs - id: 353 - area: 84 - coordinates: [15, 15] - teleporter: [191, 0] -- name: Pazuzu 2F - Pazuzu 1 - id: 354 - area: 84 - coordinates: [15, 8] - teleporter: [17, 8] -- name: Pazuzu 2F - Pazuzu 2 - id: 355 - area: 84 - coordinates: [16, 8] - teleporter: [17, 8] -- name: Pazuzu 3F Main Room - North Stairs - id: 356 - area: 85 - coordinates: [23, 11] - teleporter: [192, 0] -- name: Pazuzu 3F Main Room - West Stairs - id: 357 - area: 85 - coordinates: [7, 15] - teleporter: [193, 0] -- name: Pazuzu 3F Main Room - Pazuzu Script 1 - id: 358 - area: 85 - coordinates: [15, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Main Room - Pazuzu Script 2 - id: 359 - area: 85 - coordinates: [16, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Central Island - Central Stairs - id: 360 - area: 85 - coordinates: [15, 14] - teleporter: [194, 0] -- name: Pazuzu 3F Central Island - South Stairs - id: 361 - area: 85 - coordinates: [17, 25] - teleporter: [195, 0] -- name: Pazuzu 4F - Northwest Stairs - id: 362 - area: 86 - coordinates: [39, 12] - teleporter: [196, 0] -- name: Pazuzu 4F - Southwest Stairs - id: 363 - area: 86 - coordinates: [39, 19] - teleporter: [197, 0] -- name: Pazuzu 4F - South Stairs - id: 364 - area: 86 - coordinates: [47, 24] - teleporter: [198, 0] -- name: Pazuzu 4F - Northeast Stairs - id: 365 - area: 86 - coordinates: [54, 9] - teleporter: [199, 0] -- name: Pazuzu 4F - Pazuzu Script 1 - id: 366 - area: 86 - coordinates: [47, 8] - teleporter: [19, 8] -- name: Pazuzu 4F - Pazuzu Script 2 - id: 367 - area: 86 - coordinates: [48, 8] - teleporter: [19, 8] -- name: Pazuzu 5F Pazuzu Loop - West Stairs - id: 368 - area: 87 - coordinates: [9, 49] - teleporter: [200, 0] -- name: Pazuzu 5F Pazuzu Loop - South Stairs - id: 369 - area: 87 - coordinates: [16, 55] - teleporter: [201, 0] -- name: Pazuzu 5F Upper Loop - Northeast Stairs - id: 370 - area: 87 - coordinates: [22, 40] - teleporter: [202, 0] -- name: Pazuzu 5F Upper Loop - Northwest Stairs - id: 371 - area: 87 - coordinates: [9, 40] - teleporter: [203, 0] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 1 - id: 372 - area: 87 - coordinates: [15, 40] - teleporter: [20, 8] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 2 - id: 373 - area: 87 - coordinates: [16, 40] - teleporter: [20, 8] -- name: Pazuzu 6F - West Stairs - id: 374 - area: 88 - coordinates: [41, 47] - teleporter: [204, 0] -- name: Pazuzu 6F - Northwest Stairs - id: 375 - area: 88 - coordinates: [41, 40] - teleporter: [205, 0] -- name: Pazuzu 6F - Northeast Stairs - id: 376 - area: 88 - coordinates: [54, 40] - teleporter: [206, 0] -- name: Pazuzu 6F - South Stairs - id: 377 - area: 88 - coordinates: [52, 56] - teleporter: [207, 0] -- name: Pazuzu 6F - Pazuzu Script 1 - id: 378 - area: 88 - coordinates: [47, 40] - teleporter: [21, 8] -- name: Pazuzu 6F - Pazuzu Script 2 - id: 379 - area: 88 - coordinates: [48, 40] - teleporter: [21, 8] -- name: Pazuzu 7F Main Room - Southwest Stairs - id: 380 - area: 89 - coordinates: [15, 54] - teleporter: [26, 0] -- name: Pazuzu 7F Main Room - Northeast Stairs - id: 381 - area: 89 - coordinates: [21, 40] - teleporter: [27, 0] -- name: Pazuzu 7F Main Room - Southeast Stairs - id: 382 - area: 89 - coordinates: [21, 56] - teleporter: [28, 0] -- name: Pazuzu 7F Main Room - Pazuzu Script 1 - id: 383 - area: 89 - coordinates: [15, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Pazuzu Script 2 - id: 384 - area: 89 - coordinates: [16, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Crystal Script # Added for floor shuffle - id: 480 - area: 89 - coordinates: [15, 40] - teleporter: [38, 8] -- name: Pazuzu 1F to 3F - South Stairs - id: 385 - area: 90 - coordinates: [43, 60] - teleporter: [29, 0] -- name: Pazuzu 1F to 3F - North Stairs - id: 386 - area: 90 - coordinates: [43, 36] - teleporter: [30, 0] -- name: Pazuzu 3F to 5F - South Stairs - id: 387 - area: 91 - coordinates: [43, 60] - teleporter: [40, 0] -- name: Pazuzu 3F to 5F - North Stairs - id: 388 - area: 91 - coordinates: [43, 36] - teleporter: [41, 0] -- name: Pazuzu 5F to 7F - South Stairs - id: 389 - area: 92 - coordinates: [43, 60] - teleporter: [38, 0] -- name: Pazuzu 5F to 7F - North Stairs - id: 390 - area: 92 - coordinates: [43, 36] - teleporter: [39, 0] -- name: Pazuzu 2F to 4F - South Stairs - id: 391 - area: 93 - coordinates: [43, 60] - teleporter: [21, 0] -- name: Pazuzu 2F to 4F - North Stairs - id: 392 - area: 93 - coordinates: [43, 36] - teleporter: [22, 0] -- name: Pazuzu 4F to 6F - South Stairs - id: 393 - area: 94 - coordinates: [43, 60] - teleporter: [2, 0] -- name: Pazuzu 4F to 6F - North Stairs - id: 394 - area: 94 - coordinates: [43, 36] - teleporter: [3, 0] -- name: Light Temple - Entrance - id: 395 - area: 95 - coordinates: [28, 57] - teleporter: [19, 6] -- name: Light Temple - Mobius Teleporter Script - id: 396 - area: 95 - coordinates: [29, 37] - teleporter: [70, 8] -- name: Light Temple - Visit Quest Script 1 - id: 492 - area: 95 - coordinates: [34, 39] - teleporter: [100, 8] -- name: Light Temple - Visit Quest Script 2 - id: 493 - area: 95 - coordinates: [35, 39] - teleporter: [100, 8] -- name: Ship Dock - Mobius Teleporter Script - id: 397 - area: 96 - coordinates: [15, 18] - teleporter: [61, 8] -- name: Ship Dock - From Overworld - id: 398 - area: 96 - coordinates: [15, 11] - teleporter: [73, 0] -- name: Ship Dock - Entrance - id: 399 - area: 96 - coordinates: [15, 23] - teleporter: [17, 6] -- name: Mac Ship Deck - East Entrance Script - id: 400 - area: 97 - coordinates: [26, 40] - teleporter: [37, 8] -- name: Mac Ship Deck - Central Stairs Script - id: 401 - area: 97 - coordinates: [16, 47] - teleporter: [50, 8] -- name: Mac Ship Deck - West Stairs Script - id: 402 - area: 97 - coordinates: [8, 34] - teleporter: [51, 8] -- name: Mac Ship Deck - East Stairs Script - id: 403 - area: 97 - coordinates: [24, 36] - teleporter: [52, 8] -- name: Mac Ship Deck - North Stairs Script - id: 404 - area: 97 - coordinates: [12, 9] - teleporter: [53, 8] -- name: Mac Ship B1 Outer Ring - South Stairs - id: 405 - area: 98 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring - West Stairs - id: 406 - area: 98 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring - East Stairs - id: 407 - area: 98 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Outer Ring - Northwest Stairs - id: 408 - area: 98 - coordinates: [10, 23] - teleporter: [88, 0] -- name: Mac Ship B1 Square Room - North Stairs - id: 409 - area: 98 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room - South Stairs - id: 410 - area: 98 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room - Stairs # Unused? - id: 411 - area: 98 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor - South Stairs - id: 412 - area: 98 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor - North Stairs - id: 413 - area: 98 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B2 South Corridor - South Stairs - id: 414 - area: 99 - coordinates: [48, 51] - teleporter: [57, 1] -- name: Mac Ship B2 South Corridor - North Stairs Script - id: 415 - area: 99 - coordinates: [48, 38] - teleporter: [55, 8] -- name: Mac Ship B2 North Corridor - South Stairs Script - id: 416 - area: 99 - coordinates: [48, 27] - teleporter: [56, 8] -- name: Mac Ship B2 North Corridor - North Stairs Script - id: 417 - area: 99 - coordinates: [48, 12] - teleporter: [57, 8] -- name: Mac Ship B2 Outer Ring - Northwest Stairs Script - id: 418 - area: 99 - coordinates: [55, 11] - teleporter: [58, 8] -- name: Mac Ship B1 Outer Ring Cleared - South Stairs - id: 419 - area: 100 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring Cleared - West Stairs - id: 420 - area: 100 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring Cleared - East Stairs - id: 421 - area: 100 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Square Room Cleared - North Stairs - id: 422 - area: 100 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room Cleared - South Stairs - id: 423 - area: 100 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room Cleared - Main Stairs - id: 424 - area: 100 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor Cleared - South Stairs - id: 425 - area: 100 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor Cleared - North Stairs - id: 426 - area: 100 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B1 Central Corridor Cleared - Northwest Stairs - id: 427 - area: 100 - coordinates: [23, 10] - teleporter: [88, 0] -- name: Doom Castle Corridor of Destiny - South Entrance - id: 428 - area: 101 - coordinates: [59, 29] - teleporter: [84, 0] -- name: Doom Castle Corridor of Destiny - Ice Floor Entrance - id: 429 - area: 101 - coordinates: [59, 21] - teleporter: [35, 2] -- name: Doom Castle Corridor of Destiny - Lava Floor Entrance - id: 430 - area: 101 - coordinates: [59, 13] - teleporter: [209, 0] -- name: Doom Castle Corridor of Destiny - Sky Floor Entrance - id: 431 - area: 101 - coordinates: [59, 5] - teleporter: [211, 0] -- name: Doom Castle Corridor of Destiny - Hero Room Entrance - id: 432 - area: 101 - coordinates: [59, 61] - teleporter: [13, 2] -- name: Doom Castle Ice Floor - Entrance - id: 433 - area: 102 - coordinates: [23, 42] - teleporter: [109, 3] -- name: Doom Castle Lava Floor - Entrance - id: 434 - area: 103 - coordinates: [23, 40] - teleporter: [210, 0] -- name: Doom Castle Sky Floor - Entrance - id: 435 - area: 104 - coordinates: [24, 41] - teleporter: [212, 0] -- name: Doom Castle Hero Room - Dark King Entrance 1 - id: 436 - area: 106 - coordinates: [15, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 2 - id: 437 - area: 106 - coordinates: [16, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 3 - id: 438 - area: 106 - coordinates: [15, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 4 - id: 439 - area: 106 - coordinates: [16, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Hero Statue Script - id: 440 - area: 106 - coordinates: [15, 17] - teleporter: [24, 8] -- name: Doom Castle Hero Room - Entrance - id: 441 - area: 106 - coordinates: [15, 24] - teleporter: [110, 3] -- name: Doom Castle Dark King Room - Entrance - id: 442 - area: 107 - coordinates: [14, 26] - teleporter: [52, 0] -- name: Doom Castle Dark King Room - Dark King Script - id: 443 - area: 107 - coordinates: [14, 15] - teleporter: [25, 8] -- name: Doom Castle Dark King Room - Unknown - id: 444 - area: 107 - coordinates: [47, 54] - teleporter: [77, 0] -- name: Overworld - Level Forest - id: 445 - area: 0 - type: "Overworld" - teleporter: [0x2E, 8] -- name: Overworld - Foresta - id: 446 - area: 0 - type: "Overworld" - teleporter: [0x02, 1] -- name: Overworld - Sand Temple - id: 447 - area: 0 - type: "Overworld" - teleporter: [0x03, 1] -- name: Overworld - Bone Dungeon - id: 448 - area: 0 - type: "Overworld" - teleporter: [0x04, 1] -- name: Overworld - Focus Tower Foresta - id: 449 - area: 0 - type: "Overworld" - teleporter: [0x05, 1] -- name: Overworld - Focus Tower Aquaria - id: 450 - area: 0 - type: "Overworld" - teleporter: [0x13, 1] -- name: Overworld - Libra Temple - id: 451 - area: 0 - type: "Overworld" - teleporter: [0x07, 1] -- name: Overworld - Aquaria - id: 452 - area: 0 - type: "Overworld" - teleporter: [0x08, 8] -- name: Overworld - Wintry Cave - id: 453 - area: 0 - type: "Overworld" - teleporter: [0x0A, 1] -- name: Overworld - Life Temple - id: 454 - area: 0 - type: "Overworld" - teleporter: [0x0B, 1] -- name: Overworld - Falls Basin - id: 455 - area: 0 - type: "Overworld" - teleporter: [0x0C, 1] -- name: Overworld - Ice Pyramid - id: 456 - area: 0 - type: "Overworld" - teleporter: [0x0D, 1] # Will be switched to a script -- name: Overworld - Spencer's Place - id: 457 - area: 0 - type: "Overworld" - teleporter: [0x30, 8] -- name: Overworld - Wintry Temple - id: 458 - area: 0 - type: "Overworld" - teleporter: [0x10, 1] -- name: Overworld - Focus Tower Frozen Strip - id: 459 - area: 0 - type: "Overworld" - teleporter: [0x11, 1] -- name: Overworld - Focus Tower Fireburg - id: 460 - area: 0 - type: "Overworld" - teleporter: [0x12, 1] -- name: Overworld - Fireburg - id: 461 - area: 0 - type: "Overworld" - teleporter: [0x14, 1] -- name: Overworld - Mine - id: 462 - area: 0 - type: "Overworld" - teleporter: [0x15, 1] -- name: Overworld - Sealed Temple - id: 463 - area: 0 - type: "Overworld" - teleporter: [0x16, 1] -- name: Overworld - Volcano - id: 464 - area: 0 - type: "Overworld" - teleporter: [0x17, 1] -- name: Overworld - Lava Dome - id: 465 - area: 0 - type: "Overworld" - teleporter: [0x18, 1] -- name: Overworld - Focus Tower Windia - id: 466 - area: 0 - type: "Overworld" - teleporter: [0x06, 1] -- name: Overworld - Rope Bridge - id: 467 - area: 0 - type: "Overworld" - teleporter: [0x19, 1] -- name: Overworld - Alive Forest - id: 468 - area: 0 - type: "Overworld" - teleporter: [0x1A, 1] -- name: Overworld - Giant Tree - id: 469 - area: 0 - type: "Overworld" - teleporter: [0x1B, 1] -- name: Overworld - Kaidge Temple - id: 470 - area: 0 - type: "Overworld" - teleporter: [0x1C, 1] -- name: Overworld - Windia - id: 471 - area: 0 - type: "Overworld" - teleporter: [0x1D, 1] -- name: Overworld - Windhole Temple - id: 472 - area: 0 - type: "Overworld" - teleporter: [0x1E, 1] -- name: Overworld - Mount Gale - id: 473 - area: 0 - type: "Overworld" - teleporter: [0x1F, 1] -- name: Overworld - Pazuzu Tower - id: 474 - area: 0 - type: "Overworld" - teleporter: [0x20, 1] -- name: Overworld - Ship Dock - id: 475 - area: 0 - type: "Overworld" - teleporter: [0x3E, 1] -- name: Overworld - Doom Castle - id: 476 - area: 0 - type: "Overworld" - teleporter: [0x21, 1] -- name: Overworld - Light Temple - id: 477 - area: 0 - type: "Overworld" - teleporter: [0x22, 1] -- name: Overworld - Mac Ship - id: 478 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Overworld - Mac Ship Doom - id: 479 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Dummy House - Bed Script - id: 480 - area: 17 - coordinates: [0x28, 0x38] - teleporter: [1, 8] -- name: Dummy House - Entrance - id: 481 - area: 17 - coordinates: [0x29, 0x3B] - teleporter: [0, 10] #None diff --git a/worlds/ffmq/data/rooms.py b/worlds/ffmq/data/rooms.py new file mode 100644 index 000000000000..38634f107679 --- /dev/null +++ b/worlds/ffmq/data/rooms.py @@ -0,0 +1,2 @@ +rooms = [{'name': 'Overworld', 'id': 0, 'type': 'Overworld', 'game_objects': [], 'links': [{'target_room': 220, 'access': []}]}, {'name': 'Subregion Foresta', 'id': 220, 'type': 'Subregion', 'region': 'Foresta', 'game_objects': [{'name': 'Foresta South Battlefield', 'object_id': 1, 'location': 'ForestaSouthBattlefield', 'location_slot': 'ForestaSouthBattlefield', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Foresta West Battlefield', 'object_id': 2, 'location': 'ForestaWestBattlefield', 'location_slot': 'ForestaWestBattlefield', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Foresta East Battlefield', 'object_id': 3, 'location': 'ForestaEastBattlefield', 'location_slot': 'ForestaEastBattlefield', 'type': 'BattlefieldGp', 'access': []}], 'links': [{'target_room': 15, 'location': 'LevelForest', 'location_slot': 'LevelForest', 'entrance': 445, 'teleporter': [46, 8], 'access': []}, {'target_room': 16, 'location': 'Foresta', 'location_slot': 'Foresta', 'entrance': 446, 'teleporter': [2, 1], 'access': []}, {'target_room': 24, 'location': 'SandTemple', 'location_slot': 'SandTemple', 'entrance': 447, 'teleporter': [3, 1], 'access': []}, {'target_room': 25, 'location': 'BoneDungeon', 'location_slot': 'BoneDungeon', 'entrance': 448, 'teleporter': [4, 1], 'access': []}, {'target_room': 3, 'location': 'FocusTowerForesta', 'location_slot': 'FocusTowerForesta', 'entrance': 449, 'teleporter': [5, 1], 'access': []}, {'target_room': 221, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['RiverCoin']}, {'target_room': 226, 'access': ['SunCoin']}]}, {'name': 'Subregion Aquaria', 'id': 221, 'type': 'Subregion', 'region': 'Aquaria', 'game_objects': [{'name': 'South of Libra Temple Battlefield', 'object_id': 4, 'location': 'AquariaBattlefield01', 'location_slot': 'AquariaBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'East of Libra Temple Battlefield', 'object_id': 5, 'location': 'AquariaBattlefield02', 'location_slot': 'AquariaBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'South of Aquaria Battlefield', 'object_id': 6, 'location': 'AquariaBattlefield03', 'location_slot': 'AquariaBattlefield03', 'type': 'BattlefieldItem', 'access': []}, {'name': 'South of Wintry Cave Battlefield', 'object_id': 7, 'location': 'WintryBattlefield01', 'location_slot': 'WintryBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'West of Wintry Cave Battlefield', 'object_id': 8, 'location': 'WintryBattlefield02', 'location_slot': 'WintryBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Ice Pyramid Battlefield', 'object_id': 9, 'location': 'PyramidBattlefield01', 'location_slot': 'PyramidBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 10, 'location': 'FocusTowerAquaria', 'location_slot': 'FocusTowerAquaria', 'entrance': 450, 'teleporter': [19, 1], 'access': []}, {'target_room': 39, 'location': 'LibraTemple', 'location_slot': 'LibraTemple', 'entrance': 451, 'teleporter': [7, 1], 'access': []}, {'target_room': 40, 'location': 'Aquaria', 'location_slot': 'Aquaria', 'entrance': 452, 'teleporter': [8, 8], 'access': []}, {'target_room': 45, 'location': 'WintryCave', 'location_slot': 'WintryCave', 'entrance': 453, 'teleporter': [10, 1], 'access': []}, {'target_room': 52, 'location': 'FallsBasin', 'location_slot': 'FallsBasin', 'entrance': 455, 'teleporter': [12, 1], 'access': []}, {'target_room': 54, 'location': 'IcePyramid', 'location_slot': 'IcePyramid', 'entrance': 456, 'teleporter': [13, 1], 'access': []}, {'target_room': 220, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Life Temple', 'id': 222, 'type': 'Subregion', 'region': 'LifeTemple', 'game_objects': [], 'links': [{'target_room': 51, 'location': 'LifeTemple', 'location_slot': 'LifeTemple', 'entrance': 454, 'teleporter': [11, 1], 'access': []}]}, {'name': 'Subregion Frozen Fields', 'id': 223, 'type': 'Subregion', 'region': 'AquariaFrozenField', 'game_objects': [{'name': 'North of Libra Temple Battlefield', 'object_id': 10, 'location': 'LibraBattlefield01', 'location_slot': 'LibraBattlefield01', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Aquaria Frozen Field Battlefield', 'object_id': 11, 'location': 'LibraBattlefield02', 'location_slot': 'LibraBattlefield02', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 74, 'location': 'WintryTemple', 'location_slot': 'WintryTemple', 'entrance': 458, 'teleporter': [16, 1], 'access': []}, {'target_room': 14, 'location': 'FocusTowerFrozen', 'location_slot': 'FocusTowerFrozen', 'entrance': 459, 'teleporter': [17, 1], 'access': []}, {'target_room': 221, 'access': []}, {'target_room': 225, 'access': ['SummerAquaria', 'DualheadHydra']}]}, {'name': 'Subregion Fireburg', 'id': 224, 'type': 'Subregion', 'region': 'Fireburg', 'game_objects': [{'name': 'Path to Fireburg Southern Battlefield', 'object_id': 12, 'location': 'FireburgBattlefield01', 'location_slot': 'FireburgBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Path to Fireburg Central Battlefield', 'object_id': 13, 'location': 'FireburgBattlefield02', 'location_slot': 'FireburgBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Path to Fireburg Northern Battlefield', 'object_id': 14, 'location': 'FireburgBattlefield03', 'location_slot': 'FireburgBattlefield03', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Sealed Temple Battlefield', 'object_id': 15, 'location': 'MineBattlefield01', 'location_slot': 'MineBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Mine Battlefield', 'object_id': 16, 'location': 'MineBattlefield02', 'location_slot': 'MineBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Boulder Battlefield', 'object_id': 17, 'location': 'MineBattlefield03', 'location_slot': 'MineBattlefield03', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 13, 'location': 'FocusTowerFireburg', 'location_slot': 'FocusTowerFireburg', 'entrance': 460, 'teleporter': [18, 1], 'access': []}, {'target_room': 76, 'location': 'Fireburg', 'location_slot': 'Fireburg', 'entrance': 461, 'teleporter': [20, 1], 'access': []}, {'target_room': 84, 'location': 'Mine', 'location_slot': 'Mine', 'entrance': 462, 'teleporter': [21, 1], 'access': []}, {'target_room': 92, 'location': 'SealedTemple', 'location_slot': 'SealedTemple', 'entrance': 463, 'teleporter': [22, 1], 'access': []}, {'target_room': 93, 'location': 'Volcano', 'location_slot': 'Volcano', 'entrance': 464, 'teleporter': [23, 1], 'access': []}, {'target_room': 100, 'location': 'LavaDome', 'location_slot': 'LavaDome', 'entrance': 465, 'teleporter': [24, 1], 'access': []}, {'target_room': 220, 'access': ['RiverCoin']}, {'target_room': 221, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 225, 'access': ['DualheadHydra']}]}, {'name': 'Subregion Volcano Battlefield', 'id': 225, 'type': 'Subregion', 'region': 'VolcanoBattlefield', 'game_objects': [{'name': 'Volcano Battlefield', 'object_id': 18, 'location': 'VolcanoBattlefield01', 'location_slot': 'VolcanoBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 224, 'access': ['DualheadHydra']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Windia', 'id': 226, 'type': 'Subregion', 'region': 'Windia', 'game_objects': [{'name': 'Kaidge Temple Battlefield', 'object_id': 19, 'location': 'WindiaBattlefield01', 'location_slot': 'WindiaBattlefield01', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}, {'name': 'South of Windia Battlefield', 'object_id': 20, 'location': 'WindiaBattlefield02', 'location_slot': 'WindiaBattlefield02', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}], 'links': [{'target_room': 9, 'location': 'FocusTowerWindia', 'location_slot': 'FocusTowerWindia', 'entrance': 466, 'teleporter': [6, 1], 'access': []}, {'target_room': 123, 'location': 'RopeBridge', 'location_slot': 'RopeBridge', 'entrance': 467, 'teleporter': [25, 1], 'access': []}, {'target_room': 124, 'location': 'AliveForest', 'location_slot': 'AliveForest', 'entrance': 468, 'teleporter': [26, 1], 'access': []}, {'target_room': 125, 'location': 'GiantTree', 'location_slot': 'GiantTree', 'entrance': 469, 'teleporter': [27, 1], 'access': ['Barred']}, {'target_room': 152, 'location': 'KaidgeTemple', 'location_slot': 'KaidgeTemple', 'entrance': 470, 'teleporter': [28, 1], 'access': []}, {'target_room': 156, 'location': 'Windia', 'location_slot': 'Windia', 'entrance': 471, 'teleporter': [29, 1], 'access': []}, {'target_room': 154, 'location': 'WindholeTemple', 'location_slot': 'WindholeTemple', 'entrance': 472, 'teleporter': [30, 1], 'access': []}, {'target_room': 155, 'location': 'MountGale', 'location_slot': 'MountGale', 'entrance': 473, 'teleporter': [31, 1], 'access': []}, {'target_room': 166, 'location': 'PazuzusTower', 'location_slot': 'PazuzusTower', 'entrance': 474, 'teleporter': [32, 1], 'access': []}, {'target_room': 220, 'access': ['SunCoin']}, {'target_room': 221, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 224, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 227, 'access': ['RainbowBridge']}]}, {'name': "Subregion Spencer's Cave", 'id': 227, 'type': 'Subregion', 'region': 'SpencerCave', 'game_objects': [], 'links': [{'target_room': 73, 'location': 'SpencersPlace', 'location_slot': 'SpencersPlace', 'entrance': 457, 'teleporter': [48, 8], 'access': []}, {'target_room': 226, 'access': ['RainbowBridge']}]}, {'name': 'Subregion Ship Dock', 'id': 228, 'type': 'Subregion', 'region': 'ShipDock', 'game_objects': [], 'links': [{'target_room': 186, 'location': 'ShipDock', 'location_slot': 'ShipDock', 'entrance': 475, 'teleporter': [62, 1], 'access': []}, {'target_room': 229, 'access': ['ShipLiberated', 'ShipDockAccess']}]}, {'name': "Subregion Mac's Ship", 'id': 229, 'type': 'Subregion', 'region': 'MacShip', 'game_objects': [], 'links': [{'target_room': 187, 'location': 'MacsShip', 'location_slot': 'MacsShip', 'entrance': 478, 'teleporter': [36, 1], 'access': []}, {'target_room': 228, 'access': ['ShipLiberated', 'ShipDockAccess']}, {'target_room': 231, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Subregion Light Temple', 'id': 230, 'type': 'Subregion', 'region': 'LightTemple', 'game_objects': [], 'links': [{'target_room': 185, 'location': 'LightTemple', 'location_slot': 'LightTemple', 'entrance': 477, 'teleporter': [35, 1], 'access': []}]}, {'name': 'Subregion Doom Castle', 'id': 231, 'type': 'Subregion', 'region': 'DoomCastle', 'game_objects': [], 'links': [{'target_room': 1, 'location': 'DoomCastle', 'location_slot': 'DoomCastle', 'entrance': 476, 'teleporter': [33, 1], 'access': []}, {'target_room': 187, 'location': 'MacsShipDoom', 'location_slot': 'MacsShipDoom', 'entrance': 479, 'teleporter': [36, 1], 'access': ['Barred']}, {'target_room': 229, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Doom Castle - Sand Floor', 'id': 1, 'game_objects': [{'name': 'Doom Castle B2 - Southeast Chest', 'object_id': 1, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Doom Castle B2 - Bone Ledge Box', 'object_id': 30, 'type': 'Box', 'access': []}, {'name': 'Doom Castle B2 - Hook Platform Box', 'object_id': 31, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 231, 'entrance': 1, 'teleporter': [1, 6], 'access': []}, {'target_room': 5, 'entrance': 0, 'teleporter': [0, 0], 'access': ['DragonClaw', 'MegaGrenade']}]}, {'name': 'Doom Castle - Aero Room', 'id': 2, 'game_objects': [{'name': 'Doom Castle B2 - Sun Door Chest', 'object_id': 0, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 4, 'entrance': 2, 'teleporter': [1, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Main Loop', 'id': 3, 'game_objects': [], 'links': [{'target_room': 220, 'entrance': 3, 'teleporter': [2, 6], 'access': []}, {'target_room': 6, 'entrance': 4, 'teleporter': [4, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Aero Corridor', 'id': 4, 'game_objects': [], 'links': [{'target_room': 9, 'entrance': 5, 'teleporter': [5, 0], 'access': []}, {'target_room': 2, 'entrance': 6, 'teleporter': [8, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Inner Loop', 'id': 5, 'game_objects': [], 'links': [{'target_room': 1, 'entrance': 8, 'teleporter': [7, 0], 'access': []}, {'target_room': 201, 'entrance': 7, 'teleporter': [6, 0], 'access': []}]}, {'name': 'Focus Tower 1F Main Lobby', 'id': 6, 'game_objects': [{'name': 'Focus Tower 1F - Main Lobby Box', 'object_id': 33, 'type': 'Box', 'access': []}], 'links': [{'target_room': 3, 'entrance': 11, 'teleporter': [11, 0], 'access': []}, {'target_room': 7, 'access': ['SandCoin']}, {'target_room': 8, 'access': ['RiverCoin']}, {'target_room': 9, 'access': ['SunCoin']}]}, {'name': 'Focus Tower 1F SandCoin Room', 'id': 7, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SandCoin']}, {'target_room': 10, 'entrance': 10, 'teleporter': [10, 0], 'access': []}]}, {'name': 'Focus Tower 1F RiverCoin Room', 'id': 8, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['RiverCoin']}, {'target_room': 11, 'entrance': 14, 'teleporter': [14, 0], 'access': []}]}, {'name': 'Focus Tower 1F SunCoin Room', 'id': 9, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SunCoin']}, {'target_room': 4, 'entrance': 12, 'teleporter': [12, 0], 'access': []}, {'target_room': 226, 'entrance': 9, 'teleporter': [3, 6], 'access': []}]}, {'name': 'Focus Tower 1F SkyCoin Room', 'id': 201, 'game_objects': [], 'links': [{'target_room': 195, 'entrance': 13, 'teleporter': [13, 0], 'access': ['SkyCoin', 'FlamerusRex', 'IceGolem', 'DualheadHydra', 'Pazuzu']}, {'target_room': 5, 'entrance': 15, 'teleporter': [15, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Sand Coin Passage', 'id': 10, 'game_objects': [{'name': 'Focus Tower 2F - Sand Door Chest', 'object_id': 3, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 221, 'entrance': 16, 'teleporter': [4, 6], 'access': []}, {'target_room': 7, 'entrance': 17, 'teleporter': [17, 0], 'access': []}]}, {'name': 'Focus Tower 2F - River Coin Passage', 'id': 11, 'game_objects': [], 'links': [{'target_room': 8, 'entrance': 18, 'teleporter': [18, 0], 'access': []}, {'target_room': 13, 'entrance': 19, 'teleporter': [20, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Venus Chest Room', 'id': 12, 'game_objects': [{'name': 'Focus Tower 2F - Back Door Chest', 'object_id': 2, 'type': 'Chest', 'access': []}, {'name': 'Focus Tower 2F - Venus Chest', 'object_id': 9, 'type': 'NPC', 'access': ['Bomb', 'VenusKey']}], 'links': [{'target_room': 14, 'entrance': 20, 'teleporter': [19, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Lower Floor', 'id': 13, 'game_objects': [{'name': 'Focus Tower 3F - River Door Box', 'object_id': 34, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 22, 'teleporter': [6, 6], 'access': []}, {'target_room': 11, 'entrance': 23, 'teleporter': [24, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Upper Floor', 'id': 14, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 24, 'teleporter': [5, 6], 'access': []}, {'target_room': 12, 'entrance': 25, 'teleporter': [23, 0], 'access': []}]}, {'name': 'Level Forest', 'id': 15, 'game_objects': [{'name': 'Level Forest - Northwest Box', 'object_id': 40, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Northeast Box', 'object_id': 41, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Middle Box', 'object_id': 42, 'type': 'Box', 'access': []}, {'name': 'Level Forest - Southwest Box', 'object_id': 43, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Southeast Box', 'object_id': 44, 'type': 'Box', 'access': ['Axe']}, {'name': 'Minotaur', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Minotaur'], 'access': ['Kaeli1']}, {'name': 'Level Forest - Old Man', 'object_id': 0, 'type': 'NPC', 'access': []}, {'name': 'Level Forest - Kaeli', 'object_id': 1, 'type': 'NPC', 'access': ['Kaeli1', 'Minotaur']}], 'links': [{'target_room': 220, 'entrance': 28, 'teleporter': [25, 0], 'access': []}]}, {'name': 'Foresta', 'id': 16, 'game_objects': [{'name': 'Foresta - Outside Box', 'object_id': 45, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 220, 'entrance': 38, 'teleporter': [31, 0], 'access': []}, {'target_room': 17, 'entrance': 44, 'teleporter': [0, 5], 'access': []}, {'target_room': 18, 'entrance': 42, 'teleporter': [32, 4], 'access': []}, {'target_room': 19, 'entrance': 43, 'teleporter': [33, 0], 'access': []}, {'target_room': 20, 'entrance': 45, 'teleporter': [1, 5], 'access': []}]}, {'name': "Kaeli's House", 'id': 17, 'game_objects': [{'name': "Foresta - Kaeli's House Box", 'object_id': 46, 'type': 'Box', 'access': []}, {'name': 'Kaeli Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli1'], 'access': ['TreeWither']}, {'name': 'Kaeli 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli2'], 'access': ['Kaeli1', 'Minotaur', 'Elixir']}], 'links': [{'target_room': 16, 'entrance': 46, 'teleporter': [86, 3], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Main", 'id': 18, 'game_objects': [], 'links': [{'target_room': 19, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 47, 'teleporter': [34, 0], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Back", 'id': 19, 'game_objects': [{'name': 'Foresta - Old Man House Chest', 'object_id': 5, 'type': 'Chest', 'access': []}, {'name': 'Old Man Barrel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['BarrelPushed'], 'access': []}], 'links': [{'target_room': 18, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 48, 'teleporter': [35, 0], 'access': []}]}, {'name': 'Foresta Houses - Rest House', 'id': 20, 'game_objects': [{'name': 'Foresta - Rest House Box', 'object_id': 47, 'type': 'Box', 'access': []}], 'links': [{'target_room': 16, 'entrance': 50, 'teleporter': [87, 3], 'access': []}]}, {'name': 'Libra Treehouse', 'id': 21, 'game_objects': [{'name': 'Alive Forest - Libra Treehouse Box', 'object_id': 50, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 51, 'teleporter': [67, 8], 'access': ['LibraCrest']}]}, {'name': 'Gemini Treehouse', 'id': 22, 'game_objects': [{'name': 'Alive Forest - Gemini Treehouse Box', 'object_id': 51, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 52, 'teleporter': [68, 8], 'access': ['GeminiCrest']}]}, {'name': 'Mobius Treehouse', 'id': 23, 'game_objects': [{'name': 'Alive Forest - Mobius Treehouse West Box', 'object_id': 48, 'type': 'Box', 'access': []}, {'name': 'Alive Forest - Mobius Treehouse East Box', 'object_id': 49, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 53, 'teleporter': [69, 8], 'access': ['MobiusCrest']}]}, {'name': 'Sand Temple', 'id': 24, 'game_objects': [{'name': 'Tristam Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Tristam'], 'access': []}], 'links': [{'target_room': 220, 'entrance': 54, 'teleporter': [36, 0], 'access': []}]}, {'name': 'Bone Dungeon 1F', 'id': 25, 'game_objects': [{'name': 'Bone Dungeon 1F - Entrance Room West Box', 'object_id': 53, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room Middle Box', 'object_id': 54, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room East Box', 'object_id': 55, 'type': 'Box', 'access': []}], 'links': [{'target_room': 220, 'entrance': 55, 'teleporter': [37, 0], 'access': []}, {'target_room': 26, 'entrance': 56, 'teleporter': [2, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Waterway', 'id': 26, 'game_objects': [{'name': 'Bone Dungeon B1 - Skull Chest', 'object_id': 6, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Bone Dungeon B1 - Tristam', 'object_id': 2, 'type': 'NPC', 'access': ['Tristam']}, {'name': 'Tristam Bone Dungeon Item Given', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TristamBoneItemGiven'], 'access': ['Tristam']}], 'links': [{'target_room': 25, 'entrance': 59, 'teleporter': [88, 3], 'access': []}, {'target_room': 28, 'entrance': 57, 'teleporter': [3, 2], 'access': ['Bomb']}]}, {'name': 'Bone Dungeon B1 - Checker Room', 'id': 28, 'game_objects': [{'name': 'Bone Dungeon B1 - Checker Room Box', 'object_id': 56, 'type': 'Box', 'access': ['Bomb']}], 'links': [{'target_room': 26, 'entrance': 61, 'teleporter': [89, 3], 'access': []}, {'target_room': 30, 'entrance': 60, 'teleporter': [4, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Hidden Room', 'id': 29, 'game_objects': [{'name': 'Bone Dungeon B1 - Ribcage Waterway Box', 'object_id': 57, 'type': 'Box', 'access': []}], 'links': [{'target_room': 31, 'entrance': 62, 'teleporter': [91, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - First Room', 'id': 30, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Alcove Box', 'object_id': 59, 'type': 'Box', 'access': []}, {'name': 'Long Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LongSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 28, 'entrance': 65, 'teleporter': [90, 3], 'access': []}, {'target_room': 31, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Second Room', 'id': 31, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Looped Hallway Box', 'object_id': 58, 'type': 'Box', 'access': []}, {'name': 'Short Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShortSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 29, 'entrance': 63, 'teleporter': [5, 2], 'access': ['LongSpineBombed']}, {'target_room': 32, 'access': ['ShortSpineBombed']}, {'target_room': 30, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Third Room', 'id': 32, 'game_objects': [], 'links': [{'target_room': 35, 'entrance': 64, 'teleporter': [6, 2], 'access': []}, {'target_room': 31, 'access': ['ShortSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Box Room', 'id': 33, 'game_objects': [{'name': 'Bone Dungeon B2 - Lone Room Box', 'object_id': 61, 'type': 'Box', 'access': []}], 'links': [{'target_room': 36, 'entrance': 66, 'teleporter': [93, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Quake Room', 'id': 34, 'game_objects': [{'name': 'Bone Dungeon B2 - Penultimate Room Chest', 'object_id': 7, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 37, 'entrance': 67, 'teleporter': [94, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - First Room', 'id': 35, 'game_objects': [{'name': 'Bone Dungeon B2 - Two Skulls Room Box', 'object_id': 60, 'type': 'Box', 'access': []}, {'name': 'Skull 1', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull1Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 32, 'entrance': 71, 'teleporter': [92, 3], 'access': []}, {'target_room': 36, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Second Room', 'id': 36, 'game_objects': [{'name': 'Skull 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull2Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 33, 'entrance': 68, 'teleporter': [7, 2], 'access': []}, {'target_room': 37, 'access': ['Skull2Bombed']}, {'target_room': 35, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Third Room', 'id': 37, 'game_objects': [], 'links': [{'target_room': 34, 'entrance': 69, 'teleporter': [8, 2], 'access': []}, {'target_room': 38, 'entrance': 70, 'teleporter': [9, 2], 'access': ['Bomb']}, {'target_room': 36, 'access': ['Skull2Bombed']}]}, {'name': 'Bone Dungeon B2 - Boss Room', 'id': 38, 'game_objects': [{'name': 'Bone Dungeon B2 - North Box', 'object_id': 62, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - South Box', 'object_id': 63, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - Flamerus Rex Chest', 'object_id': 8, 'type': 'Chest', 'access': []}, {'name': "Bone Dungeon B2 - Tristam's Treasure Chest", 'object_id': 4, 'type': 'Chest', 'access': []}, {'name': 'Flamerus Rex', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FlamerusRex'], 'access': []}], 'links': [{'target_room': 37, 'entrance': 74, 'teleporter': [95, 3], 'access': []}]}, {'name': 'Libra Temple', 'id': 39, 'game_objects': [{'name': 'Libra Temple - Box', 'object_id': 64, 'type': 'Box', 'access': []}, {'name': 'Phoebe Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phoebe1'], 'access': []}], 'links': [{'target_room': 221, 'entrance': 75, 'teleporter': [13, 6], 'access': []}, {'target_room': 51, 'entrance': 76, 'teleporter': [59, 8], 'access': ['LibraCrest']}]}, {'name': 'Aquaria', 'id': 40, 'game_objects': [{'name': 'Summer Aquaria', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SummerAquaria'], 'access': ['WakeWater']}], 'links': [{'target_room': 221, 'entrance': 77, 'teleporter': [8, 6], 'access': []}, {'target_room': 41, 'entrance': 81, 'teleporter': [10, 5], 'access': []}, {'target_room': 42, 'entrance': 82, 'teleporter': [44, 4], 'access': []}, {'target_room': 44, 'entrance': 83, 'teleporter': [11, 5], 'access': []}, {'target_room': 71, 'entrance': 89, 'teleporter': [42, 0], 'access': ['SummerAquaria']}, {'target_room': 71, 'entrance': 90, 'teleporter': [43, 0], 'access': ['SummerAquaria']}]}, {'name': "Phoebe's House", 'id': 41, 'game_objects': [{'name': "Aquaria - Phoebe's House Chest", 'object_id': 65, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 93, 'teleporter': [5, 8], 'access': []}]}, {'name': 'Aquaria Vendor House', 'id': 42, 'game_objects': [{'name': 'Aquaria - Vendor', 'object_id': 4, 'type': 'NPC', 'access': []}, {'name': 'Aquaria - Vendor House Box', 'object_id': 66, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 94, 'teleporter': [40, 8], 'access': []}, {'target_room': 43, 'entrance': 95, 'teleporter': [47, 0], 'access': []}]}, {'name': 'Aquaria Gemini Room', 'id': 43, 'game_objects': [], 'links': [{'target_room': 42, 'entrance': 97, 'teleporter': [48, 0], 'access': []}, {'target_room': 81, 'entrance': 96, 'teleporter': [72, 8], 'access': ['GeminiCrest']}]}, {'name': 'Aquaria INN', 'id': 44, 'game_objects': [], 'links': [{'target_room': 40, 'entrance': 98, 'teleporter': [75, 8], 'access': []}]}, {'name': 'Wintry Cave 1F - East Ledge', 'id': 45, 'game_objects': [{'name': 'Wintry Cave 1F - North Box', 'object_id': 67, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Entrance Box', 'object_id': 70, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Slippery Cliff Box', 'object_id': 68, 'type': 'Box', 'access': ['Claw']}, {'name': 'Wintry Cave 1F - Phoebe', 'object_id': 5, 'type': 'NPC', 'access': ['Phoebe1']}], 'links': [{'target_room': 221, 'entrance': 99, 'teleporter': [49, 0], 'access': []}, {'target_room': 49, 'entrance': 100, 'teleporter': [14, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - Central Space', 'id': 46, 'game_objects': [{'name': 'Wintry Cave 1F - Scenic Overlook Box', 'object_id': 69, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 45, 'access': ['Claw']}, {'target_room': 47, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - West Ledge', 'id': 47, 'game_objects': [], 'links': [{'target_room': 48, 'entrance': 101, 'teleporter': [15, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 2F', 'id': 48, 'game_objects': [{'name': 'Wintry Cave 2F - West Left Box', 'object_id': 71, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - West Right Box', 'object_id': 72, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Left Box', 'object_id': 73, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Right Box', 'object_id': 74, 'type': 'Box', 'access': []}], 'links': [{'target_room': 47, 'entrance': 104, 'teleporter': [97, 3], 'access': []}, {'target_room': 50, 'entrance': 103, 'teleporter': [50, 0], 'access': []}]}, {'name': 'Wintry Cave 3F Top', 'id': 49, 'game_objects': [{'name': 'Wintry Cave 3F - West Box', 'object_id': 75, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 3F - East Box', 'object_id': 76, 'type': 'Box', 'access': []}], 'links': [{'target_room': 45, 'entrance': 105, 'teleporter': [96, 3], 'access': []}]}, {'name': 'Wintry Cave 3F Bottom', 'id': 50, 'game_objects': [{'name': 'Wintry Cave 3F - Squidite Chest', 'object_id': 9, 'type': 'Chest', 'access': ['Phanquid']}, {'name': 'Phanquid', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phanquid'], 'access': []}, {'name': 'Wintry Cave 3F - Before Boss Box', 'object_id': 77, 'type': 'Box', 'access': []}], 'links': [{'target_room': 48, 'entrance': 106, 'teleporter': [51, 0], 'access': []}]}, {'name': 'Life Temple', 'id': 51, 'game_objects': [{'name': 'Life Temple - Box', 'object_id': 78, 'type': 'Box', 'access': []}, {'name': 'Life Temple - Mysterious Man', 'object_id': 6, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 222, 'entrance': 107, 'teleporter': [14, 6], 'access': []}, {'target_room': 39, 'entrance': 108, 'teleporter': [60, 8], 'access': ['LibraCrest']}]}, {'name': 'Fall Basin', 'id': 52, 'game_objects': [{'name': 'Falls Basin - Snow Crab Chest', 'object_id': 10, 'type': 'Chest', 'access': ['FreezerCrab']}, {'name': 'Freezer Crab', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FreezerCrab'], 'access': []}, {'name': 'Falls Basin - Box', 'object_id': 79, 'type': 'Box', 'access': []}], 'links': [{'target_room': 221, 'entrance': 111, 'teleporter': [53, 0], 'access': []}]}, {'name': 'Ice Pyramid B1 Taunt Room', 'id': 53, 'game_objects': [{'name': 'Ice Pyramid B1 - Chest', 'object_id': 11, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid B1 - West Box', 'object_id': 80, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - North Box', 'object_id': 81, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - East Box', 'object_id': 82, 'type': 'Box', 'access': []}], 'links': [{'target_room': 68, 'entrance': 113, 'teleporter': [55, 0], 'access': []}]}, {'name': 'Ice Pyramid 1F Maze Lobby', 'id': 54, 'game_objects': [{'name': 'Ice Pyramid 1F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid1FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 221, 'entrance': 114, 'teleporter': [56, 0], 'access': []}, {'target_room': 55, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 1F Maze', 'id': 55, 'game_objects': [{'name': 'Ice Pyramid 1F - East Alcove Chest', 'object_id': 13, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 1F - Sandwiched Alcove Box', 'object_id': 83, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Left Box', 'object_id': 84, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Right Box', 'object_id': 85, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 116, 'teleporter': [57, 0], 'access': []}, {'target_room': 57, 'entrance': 117, 'teleporter': [58, 0], 'access': []}, {'target_room': 58, 'entrance': 118, 'teleporter': [59, 0], 'access': []}, {'target_room': 59, 'entrance': 119, 'teleporter': [60, 0], 'access': []}, {'target_room': 60, 'entrance': 120, 'teleporter': [61, 0], 'access': []}, {'target_room': 54, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 2F South Tiled Room', 'id': 56, 'game_objects': [{'name': 'Ice Pyramid 2F - South Side Glass Door Box', 'object_id': 87, 'type': 'Box', 'access': ['Sword']}, {'name': 'Ice Pyramid 2F - South Side East Box', 'object_id': 91, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 122, 'teleporter': [62, 0], 'access': []}, {'target_room': 61, 'entrance': 123, 'teleporter': [67, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F West Room', 'id': 57, 'game_objects': [{'name': 'Ice Pyramid 2F - Northwest Room Box', 'object_id': 90, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 124, 'teleporter': [63, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Center Room', 'id': 58, 'game_objects': [{'name': 'Ice Pyramid 2F - Center Room Box', 'object_id': 86, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 125, 'teleporter': [64, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Small North Room', 'id': 59, 'game_objects': [{'name': 'Ice Pyramid 2F - North Room Glass Door Box', 'object_id': 88, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 126, 'teleporter': [65, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F North Corridor', 'id': 60, 'game_objects': [{'name': 'Ice Pyramid 2F - North Corridor Glass Door Box', 'object_id': 89, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 127, 'teleporter': [66, 0], 'access': []}, {'target_room': 62, 'entrance': 128, 'teleporter': [68, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Two Boxes Room', 'id': 61, 'game_objects': [{'name': 'Ice Pyramid 3F - Staircase Dead End Left Box', 'object_id': 94, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Staircase Dead End Right Box', 'object_id': 95, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 129, 'teleporter': [69, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Main Loop', 'id': 62, 'game_objects': [{'name': 'Ice Pyramid 3F - Inner Room North Box', 'object_id': 92, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Inner Room South Box', 'object_id': 93, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - East Alcove Box', 'object_id': 96, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Leapfrog Box', 'object_id': 97, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid3FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 60, 'entrance': 130, 'teleporter': [70, 0], 'access': []}, {'target_room': 63, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 3F Blocked Room', 'id': 63, 'game_objects': [], 'links': [{'target_room': 64, 'entrance': 131, 'teleporter': [71, 0], 'access': []}, {'target_room': 62, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 4F Main Loop', 'id': 64, 'game_objects': [], 'links': [{'target_room': 66, 'entrance': 133, 'teleporter': [73, 0], 'access': []}, {'target_room': 63, 'entrance': 132, 'teleporter': [72, 0], 'access': []}, {'target_room': 65, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 4F Treasure Room', 'id': 65, 'game_objects': [{'name': 'Ice Pyramid 4F - Chest', 'object_id': 12, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 4F - Northwest Box', 'object_id': 98, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Left Box', 'object_id': 99, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Right Box', 'object_id': 100, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Left Box', 'object_id': 101, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Right Box', 'object_id': 102, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Left Box', 'object_id': 103, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Right Box', 'object_id': 104, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid4FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 64, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 5F Leap of Faith Room', 'id': 66, 'game_objects': [{'name': 'Ice Pyramid 5F - Glass Door Left Box', 'object_id': 105, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - West Ledge Box', 'object_id': 106, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Shelf Box', 'object_id': 107, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Leapfrog Box', 'object_id': 108, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - Glass Door Right Box', 'object_id': 109, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - North Box', 'object_id': 110, 'type': 'Box', 'access': []}], 'links': [{'target_room': 64, 'entrance': 134, 'teleporter': [74, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 53, 'access': ['Bomb', 'Claw', 'Sword']}]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem', 'id': 67, 'game_objects': [{'name': 'Ice Pyramid 5F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid5FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 69, 'entrance': 137, 'teleporter': [76, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 70, 'entrance': 136, 'teleporter': [75, 0], 'access': []}]}, {'name': 'Ice Pyramid Climbing Wall Room Lower Space', 'id': 68, 'game_objects': [], 'links': [{'target_room': 53, 'entrance': 139, 'teleporter': [78, 0], 'access': []}, {'target_room': 69, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Climbing Wall Room Upper Space', 'id': 69, 'game_objects': [], 'links': [{'target_room': 67, 'entrance': 140, 'teleporter': [79, 0], 'access': []}, {'target_room': 68, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Ice Golem Room', 'id': 70, 'game_objects': [{'name': 'Ice Pyramid 6F - Ice Golem Chest', 'object_id': 14, 'type': 'Chest', 'access': ['IceGolem']}, {'name': 'Ice Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IceGolem'], 'access': []}], 'links': [{'target_room': 67, 'entrance': 141, 'teleporter': [80, 0], 'access': []}, {'target_room': 66, 'access': []}]}, {'name': 'Spencer Waterfall', 'id': 71, 'game_objects': [], 'links': [{'target_room': 72, 'entrance': 143, 'teleporter': [81, 0], 'access': []}, {'target_room': 40, 'entrance': 145, 'teleporter': [82, 0], 'access': []}, {'target_room': 40, 'entrance': 148, 'teleporter': [83, 0], 'access': []}]}, {'name': 'Spencer Cave Normal Main', 'id': 72, 'game_objects': [{'name': "Spencer's Cave - Box", 'object_id': 111, 'type': 'Box', 'access': ['Claw']}, {'name': "Spencer's Cave - Spencer", 'object_id': 8, 'type': 'NPC', 'access': []}, {'name': "Spencer's Cave - Locked Chest", 'object_id': 13, 'type': 'NPC', 'access': ['VenusKey']}], 'links': [{'target_room': 71, 'entrance': 150, 'teleporter': [85, 0], 'access': []}]}, {'name': 'Spencer Cave Normal South Ledge', 'id': 73, 'game_objects': [{'name': "Collapse Spencer's Cave", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLiberated'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 227, 'entrance': 151, 'teleporter': [7, 6], 'access': []}, {'target_room': 203, 'access': ['MegaGrenade']}]}, {'name': 'Spencer Cave Caved In Main Loop', 'id': 203, 'game_objects': [], 'links': [{'target_room': 73, 'access': []}, {'target_room': 207, 'entrance': 156, 'teleporter': [36, 8], 'access': ['MobiusCrest']}, {'target_room': 204, 'access': ['Claw']}, {'target_room': 205, 'access': ['Bomb']}]}, {'name': 'Spencer Cave Caved In Waters', 'id': 204, 'game_objects': [{'name': 'Bomb Libra Block', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SpencerCaveLibraBlockBombed'], 'access': ['MegaGrenade', 'Claw']}], 'links': [{'target_room': 203, 'access': ['Claw']}]}, {'name': 'Spencer Cave Caved In Libra Nook', 'id': 205, 'game_objects': [], 'links': [{'target_room': 206, 'entrance': 153, 'teleporter': [33, 8], 'access': ['LibraCrest']}]}, {'name': 'Spencer Cave Caved In Libra Corridor', 'id': 206, 'game_objects': [], 'links': [{'target_room': 205, 'entrance': 154, 'teleporter': [34, 8], 'access': ['LibraCrest']}, {'target_room': 207, 'access': ['SpencerCaveLibraBlockBombed']}]}, {'name': 'Spencer Cave Caved In Mobius Chest', 'id': 207, 'game_objects': [{'name': "Spencer's Cave - Mobius Chest", 'object_id': 15, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 203, 'entrance': 155, 'teleporter': [35, 8], 'access': ['MobiusCrest']}, {'target_room': 206, 'access': ['Bomb']}]}, {'name': 'Wintry Temple Outer Room', 'id': 74, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 157, 'teleporter': [15, 6], 'access': []}]}, {'name': 'Wintry Temple Inner Room', 'id': 75, 'game_objects': [{'name': 'Wintry Temple - West Box', 'object_id': 112, 'type': 'Box', 'access': []}, {'name': 'Wintry Temple - North Box', 'object_id': 113, 'type': 'Box', 'access': []}], 'links': [{'target_room': 92, 'entrance': 158, 'teleporter': [62, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Upper Plaza', 'id': 76, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 159, 'teleporter': [9, 6], 'access': []}, {'target_room': 80, 'entrance': 163, 'teleporter': [91, 0], 'access': []}, {'target_room': 77, 'entrance': 164, 'teleporter': [98, 8], 'access': []}, {'target_room': 82, 'entrance': 165, 'teleporter': [96, 8], 'access': []}, {'target_room': 208, 'access': ['Claw']}]}, {'name': 'Fireburg Lower Plaza', 'id': 208, 'game_objects': [{'name': 'Fireburg - Hidden Tunnel Box', 'object_id': 116, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'access': ['Claw']}, {'target_room': 78, 'entrance': 166, 'teleporter': [11, 8], 'access': ['MultiKey']}]}, {'name': "Reuben's House", 'id': 77, 'game_objects': [{'name': "Fireburg - Reuben's House Arion", 'object_id': 14, 'type': 'NPC', 'access': ['ReubenDadSaved']}, {'name': 'Reuben Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Reuben1'], 'access': []}, {'name': "Fireburg - Reuben's House Box", 'object_id': 117, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'entrance': 167, 'teleporter': [98, 3], 'access': []}]}, {'name': "GrenadeMan's House", 'id': 78, 'game_objects': [{'name': 'Fireburg - Locked House Man', 'object_id': 12, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 208, 'entrance': 168, 'teleporter': [9, 8], 'access': ['MultiKey']}, {'target_room': 79, 'entrance': 169, 'teleporter': [93, 0], 'access': []}]}, {'name': "GrenadeMan's Mobius Room", 'id': 79, 'game_objects': [], 'links': [{'target_room': 78, 'entrance': 170, 'teleporter': [94, 0], 'access': []}, {'target_room': 161, 'entrance': 171, 'teleporter': [54, 8], 'access': ['MobiusCrest']}]}, {'name': 'Fireburg Vendor House', 'id': 80, 'game_objects': [{'name': 'Fireburg - Vendor', 'object_id': 11, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 76, 'entrance': 172, 'teleporter': [95, 0], 'access': []}, {'target_room': 81, 'entrance': 173, 'teleporter': [96, 0], 'access': []}]}, {'name': 'Fireburg Gemini Room', 'id': 81, 'game_objects': [], 'links': [{'target_room': 80, 'entrance': 174, 'teleporter': [97, 0], 'access': []}, {'target_room': 43, 'entrance': 175, 'teleporter': [45, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Hotel Lobby', 'id': 82, 'game_objects': [{'name': 'Fireburg - Tristam', 'object_id': 10, 'type': 'NPC', 'access': ['Tristam', 'TristamBoneItemGiven']}], 'links': [{'target_room': 76, 'entrance': 177, 'teleporter': [99, 3], 'access': []}, {'target_room': 83, 'entrance': 176, 'teleporter': [213, 0], 'access': []}]}, {'name': 'Fireburg Hotel Beds', 'id': 83, 'game_objects': [], 'links': [{'target_room': 82, 'entrance': 178, 'teleporter': [214, 0], 'access': []}]}, {'name': 'Mine Exterior North West Platforms', 'id': 84, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 179, 'teleporter': [98, 0], 'access': []}, {'target_room': 88, 'entrance': 181, 'teleporter': [20, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}, {'target_room': 86, 'access': ['Claw']}, {'target_room': 87, 'access': ['Claw']}]}, {'name': 'Mine Exterior Central Ledge', 'id': 85, 'game_objects': [], 'links': [{'target_room': 90, 'entrance': 183, 'teleporter': [22, 2], 'access': ['Bomb']}, {'target_room': 84, 'access': ['Claw']}]}, {'name': 'Mine Exterior North Ledge', 'id': 86, 'game_objects': [], 'links': [{'target_room': 89, 'entrance': 182, 'teleporter': [21, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Exterior South East Platforms', 'id': 87, 'game_objects': [{'name': 'Jinn', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Jinn'], 'access': []}], 'links': [{'target_room': 91, 'entrance': 180, 'teleporter': [99, 0], 'access': ['Jinn']}, {'target_room': 86, 'access': []}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Parallel Room', 'id': 88, 'game_objects': [{'name': 'Mine - Parallel Room West Box', 'object_id': 119, 'type': 'Box', 'access': ['Claw']}, {'name': 'Mine - Parallel Room East Box', 'object_id': 120, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 84, 'entrance': 185, 'teleporter': [100, 3], 'access': []}]}, {'name': 'Mine Crescent Room', 'id': 89, 'game_objects': [{'name': 'Mine - Crescent Room Chest', 'object_id': 16, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 86, 'entrance': 186, 'teleporter': [101, 3], 'access': []}]}, {'name': 'Mine Climbing Room', 'id': 90, 'game_objects': [{'name': 'Mine - Glitchy Collision Cave Box', 'object_id': 118, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 85, 'entrance': 187, 'teleporter': [102, 3], 'access': []}]}, {'name': 'Mine Cliff', 'id': 91, 'game_objects': [{'name': 'Mine - Cliff Southwest Box', 'object_id': 121, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northwest Box', 'object_id': 122, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northeast Box', 'object_id': 123, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Southeast Box', 'object_id': 124, 'type': 'Box', 'access': []}, {'name': 'Mine - Reuben', 'object_id': 7, 'type': 'NPC', 'access': ['Reuben1']}, {'name': "Reuben's dad Saved", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ReubenDadSaved'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 87, 'entrance': 188, 'teleporter': [100, 0], 'access': []}]}, {'name': 'Sealed Temple', 'id': 92, 'game_objects': [{'name': 'Sealed Temple - West Box', 'object_id': 125, 'type': 'Box', 'access': []}, {'name': 'Sealed Temple - East Box', 'object_id': 126, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 190, 'teleporter': [16, 6], 'access': []}, {'target_room': 75, 'entrance': 191, 'teleporter': [63, 8], 'access': ['GeminiCrest']}]}, {'name': 'Volcano Base', 'id': 93, 'game_objects': [{'name': 'Volcano - Base Chest', 'object_id': 17, 'type': 'Chest', 'access': []}, {'name': 'Volcano - Base West Box', 'object_id': 127, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Left Box', 'object_id': 128, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Right Box', 'object_id': 129, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 192, 'teleporter': [103, 0], 'access': []}, {'target_room': 98, 'entrance': 196, 'teleporter': [31, 8], 'access': []}, {'target_room': 96, 'entrance': 197, 'teleporter': [30, 8], 'access': []}]}, {'name': 'Volcano Top Left', 'id': 94, 'game_objects': [{'name': 'Volcano - Medusa Chest', 'object_id': 18, 'type': 'Chest', 'access': ['Medusa']}, {'name': 'Medusa', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Medusa'], 'access': []}, {'name': 'Volcano - Behind Medusa Box', 'object_id': 130, 'type': 'Box', 'access': []}], 'links': [{'target_room': 209, 'entrance': 199, 'teleporter': [26, 8], 'access': []}]}, {'name': 'Volcano Top Right', 'id': 95, 'game_objects': [{'name': 'Volcano - Top of the Volcano Left Box', 'object_id': 131, 'type': 'Box', 'access': []}, {'name': 'Volcano - Top of the Volcano Right Box', 'object_id': 132, 'type': 'Box', 'access': []}], 'links': [{'target_room': 99, 'entrance': 200, 'teleporter': [79, 8], 'access': []}]}, {'name': 'Volcano Right Path', 'id': 96, 'game_objects': [{'name': 'Volcano - Right Path Box', 'object_id': 135, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 201, 'teleporter': [15, 8], 'access': []}]}, {'name': 'Volcano Left Path', 'id': 98, 'game_objects': [{'name': 'Volcano - Left Path Box', 'object_id': 134, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 204, 'teleporter': [27, 8], 'access': []}, {'target_room': 99, 'entrance': 202, 'teleporter': [25, 2], 'access': []}, {'target_room': 209, 'entrance': 203, 'teleporter': [26, 2], 'access': []}]}, {'name': 'Volcano Cross Left-Right', 'id': 99, 'game_objects': [], 'links': [{'target_room': 95, 'entrance': 206, 'teleporter': [29, 8], 'access': []}, {'target_room': 98, 'entrance': 205, 'teleporter': [103, 3], 'access': []}]}, {'name': 'Volcano Cross Right-Left', 'id': 209, 'game_objects': [{'name': 'Volcano - Crossover Section Box', 'object_id': 133, 'type': 'Box', 'access': []}], 'links': [{'target_room': 98, 'entrance': 208, 'teleporter': [104, 3], 'access': []}, {'target_room': 94, 'entrance': 207, 'teleporter': [28, 8], 'access': []}]}, {'name': 'Lava Dome Inner Ring Main Loop', 'id': 100, 'game_objects': [{'name': 'Lava Dome - Exterior Caldera Near Switch Cliff Box', 'object_id': 136, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Exterior South Cliff Box', 'object_id': 137, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 209, 'teleporter': [104, 0], 'access': []}, {'target_room': 113, 'entrance': 211, 'teleporter': [105, 0], 'access': []}, {'target_room': 114, 'entrance': 212, 'teleporter': [106, 0], 'access': []}, {'target_room': 116, 'entrance': 213, 'teleporter': [108, 0], 'access': []}, {'target_room': 118, 'entrance': 214, 'teleporter': [111, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Center Ledge', 'id': 101, 'game_objects': [{'name': 'Lava Dome - Exterior Center Dropoff Ledge Box', 'object_id': 138, 'type': 'Box', 'access': []}], 'links': [{'target_room': 115, 'entrance': 215, 'teleporter': [107, 0], 'access': []}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Plate Ledge', 'id': 102, 'game_objects': [{'name': 'Lava Dome Plate', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LavaDomePlate'], 'access': []}], 'links': [{'target_room': 119, 'entrance': 216, 'teleporter': [109, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Upper Ledge West', 'id': 103, 'game_objects': [], 'links': [{'target_room': 111, 'entrance': 219, 'teleporter': [112, 0], 'access': []}, {'target_room': 108, 'entrance': 220, 'teleporter': [113, 0], 'access': []}, {'target_room': 104, 'access': ['Claw']}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Upper Ledge East', 'id': 104, 'game_objects': [], 'links': [{'target_room': 110, 'entrance': 218, 'teleporter': [110, 0], 'access': []}, {'target_room': 103, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Big Door Ledge', 'id': 105, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 221, 'teleporter': [114, 0], 'access': []}, {'target_room': 121, 'entrance': 222, 'teleporter': [29, 2], 'access': ['LavaDomePlate']}]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge', 'id': 106, 'game_objects': [{'name': 'Lava Dome - Exterior Dead End Caldera Box', 'object_id': 139, 'type': 'Box', 'access': []}], 'links': [{'target_room': 120, 'entrance': 226, 'teleporter': [115, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze II', 'id': 107, 'game_objects': [{'name': 'Lava Dome - Gold Maze Northwest Box', 'object_id': 140, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southwest Box', 'object_id': 246, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Northeast Box', 'object_id': 247, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze North Box', 'object_id': 248, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Center Box', 'object_id': 249, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southeast Box', 'object_id': 250, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 227, 'teleporter': [116, 0], 'access': []}, {'target_room': 108, 'entrance': 228, 'teleporter': [119, 0], 'access': []}, {'target_room': 120, 'entrance': 229, 'teleporter': [120, 0], 'access': []}]}, {'name': 'Lava Dome Up-Down Corridor', 'id': 108, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 231, 'teleporter': [118, 0], 'access': []}, {'target_room': 103, 'entrance': 230, 'teleporter': [117, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze I', 'id': 109, 'game_objects': [{'name': 'Lava Dome - Bare Maze Leapfrog Alcove North Box', 'object_id': 141, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Leapfrog Alcove South Box', 'object_id': 142, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Center Box', 'object_id': 143, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Southwest Box', 'object_id': 144, 'type': 'Box', 'access': []}], 'links': [{'target_room': 118, 'entrance': 232, 'teleporter': [121, 0], 'access': []}, {'target_room': 111, 'entrance': 233, 'teleporter': [122, 0], 'access': []}]}, {'name': 'Lava Dome Pointless Room', 'id': 110, 'game_objects': [], 'links': [{'target_room': 104, 'entrance': 234, 'teleporter': [123, 0], 'access': []}]}, {'name': 'Lava Dome Lower Moon Helm Room', 'id': 111, 'game_objects': [{'name': 'Lava Dome - U-Bend Room North Box', 'object_id': 146, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - U-Bend Room South Box', 'object_id': 147, 'type': 'Box', 'access': []}], 'links': [{'target_room': 103, 'entrance': 235, 'teleporter': [124, 0], 'access': []}, {'target_room': 109, 'entrance': 236, 'teleporter': [125, 0], 'access': []}]}, {'name': 'Lava Dome Moon Helm Room', 'id': 112, 'game_objects': [{'name': 'Lava Dome - Beyond River Room Chest', 'object_id': 19, 'type': 'Chest', 'access': []}, {'name': 'Lava Dome - Beyond River Room Box', 'object_id': 145, 'type': 'Box', 'access': []}], 'links': [{'target_room': 117, 'entrance': 237, 'teleporter': [126, 0], 'access': []}]}, {'name': 'Lava Dome Three Jumps Room', 'id': 113, 'game_objects': [{'name': 'Lava Dome - Three Jumps Room Box', 'object_id': 150, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 238, 'teleporter': [127, 0], 'access': []}]}, {'name': 'Lava Dome Life Chest Room Lower Ledge', 'id': 114, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Boulder Chest', 'object_id': 28, 'type': 'Chest', 'access': ['MegaGrenade']}], 'links': [{'target_room': 100, 'entrance': 239, 'teleporter': [128, 0], 'access': []}, {'target_room': 115, 'access': ['Claw']}]}, {'name': 'Lava Dome Life Chest Room Upper Ledge', 'id': 115, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box West', 'object_id': 148, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box East', 'object_id': 149, 'type': 'Box', 'access': []}], 'links': [{'target_room': 101, 'entrance': 240, 'teleporter': [129, 0], 'access': []}, {'target_room': 114, 'access': ['Claw']}]}, {'name': 'Lava Dome Big Jump Room Main Area', 'id': 116, 'game_objects': [{'name': 'Lava Dome - Lava River Room North Box', 'object_id': 152, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room East Box', 'object_id': 153, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room South Box', 'object_id': 154, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 241, 'teleporter': [133, 0], 'access': []}, {'target_room': 119, 'entrance': 243, 'teleporter': [132, 0], 'access': []}, {'target_room': 117, 'access': ['MegaGrenade']}]}, {'name': 'Lava Dome Big Jump Room MegaGrenade Area', 'id': 117, 'game_objects': [], 'links': [{'target_room': 112, 'entrance': 242, 'teleporter': [131, 0], 'access': []}, {'target_room': 116, 'access': ['Bomb']}]}, {'name': 'Lava Dome Split Corridor', 'id': 118, 'game_objects': [{'name': 'Lava Dome - Split Corridor Box', 'object_id': 151, 'type': 'Box', 'access': []}], 'links': [{'target_room': 109, 'entrance': 244, 'teleporter': [130, 0], 'access': []}, {'target_room': 100, 'entrance': 245, 'teleporter': [134, 0], 'access': []}]}, {'name': 'Lava Dome Plate Corridor', 'id': 119, 'game_objects': [], 'links': [{'target_room': 102, 'entrance': 246, 'teleporter': [135, 0], 'access': []}, {'target_room': 116, 'entrance': 247, 'teleporter': [137, 0], 'access': []}]}, {'name': 'Lava Dome Four Boxes Stairs', 'id': 120, 'game_objects': [{'name': 'Lava Dome - Caldera Stairway West Left Box', 'object_id': 155, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway West Right Box', 'object_id': 156, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Left Box', 'object_id': 157, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Right Box', 'object_id': 158, 'type': 'Box', 'access': []}], 'links': [{'target_room': 107, 'entrance': 248, 'teleporter': [136, 0], 'access': []}, {'target_room': 106, 'entrance': 249, 'teleporter': [16, 0], 'access': []}]}, {'name': 'Lava Dome Hydra Room', 'id': 121, 'game_objects': [{'name': 'Lava Dome - Dualhead Hydra Chest', 'object_id': 20, 'type': 'Chest', 'access': ['DualheadHydra']}, {'name': 'Dualhead Hydra', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['DualheadHydra'], 'access': []}, {'name': 'Lava Dome - Hydra Room Northwest Box', 'object_id': 159, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Hydra Room Southweast Box', 'object_id': 160, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 250, 'teleporter': [105, 3], 'access': []}, {'target_room': 122, 'entrance': 251, 'teleporter': [138, 0], 'access': ['DualheadHydra']}]}, {'name': 'Lava Dome Escape Corridor', 'id': 122, 'game_objects': [], 'links': [{'target_room': 121, 'entrance': 253, 'teleporter': [139, 0], 'access': []}]}, {'name': 'Rope Bridge', 'id': 123, 'game_objects': [{'name': 'Rope Bridge - West Box', 'object_id': 163, 'type': 'Box', 'access': []}, {'name': 'Rope Bridge - East Box', 'object_id': 164, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 255, 'teleporter': [140, 0], 'access': []}]}, {'name': 'Alive Forest', 'id': 124, 'game_objects': [{'name': 'Alive Forest - Tree Stump Chest', 'object_id': 21, 'type': 'Chest', 'access': ['Axe']}, {'name': 'Alive Forest - Near Entrance Box', 'object_id': 165, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - After Bridge Box', 'object_id': 166, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - Gemini Stump Box', 'object_id': 167, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 226, 'entrance': 272, 'teleporter': [142, 0], 'access': ['Axe']}, {'target_room': 21, 'entrance': 275, 'teleporter': [64, 8], 'access': ['LibraCrest', 'Axe']}, {'target_room': 22, 'entrance': 276, 'teleporter': [65, 8], 'access': ['GeminiCrest', 'Axe']}, {'target_room': 23, 'entrance': 277, 'teleporter': [66, 8], 'access': ['MobiusCrest', 'Axe']}, {'target_room': 125, 'entrance': 274, 'teleporter': [143, 0], 'access': ['Axe']}]}, {'name': 'Giant Tree 1F Main Area', 'id': 125, 'game_objects': [{'name': 'Giant Tree 1F - Northwest Box', 'object_id': 168, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Southwest Box', 'object_id': 169, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Center Box', 'object_id': 170, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - East Box', 'object_id': 171, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 278, 'teleporter': [56, 1], 'access': []}, {'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F North Island', 'id': 202, 'game_objects': [], 'links': [{'target_room': 127, 'entrance': 280, 'teleporter': [144, 0], 'access': []}, {'target_room': 125, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F Central Island', 'id': 126, 'game_objects': [], 'links': [{'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Main Lobby', 'id': 127, 'game_objects': [{'name': 'Giant Tree 2F - North Box', 'object_id': 172, 'type': 'Box', 'access': []}], 'links': [{'target_room': 126, 'access': ['DragonClaw']}, {'target_room': 125, 'entrance': 281, 'teleporter': [145, 0], 'access': []}, {'target_room': 133, 'entrance': 283, 'teleporter': [149, 0], 'access': []}, {'target_room': 129, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F West Ledge', 'id': 128, 'game_objects': [{'name': 'Giant Tree 2F - Dropdown Ledge Box', 'object_id': 174, 'type': 'Box', 'access': []}], 'links': [{'target_room': 140, 'entrance': 284, 'teleporter': [147, 0], 'access': ['Sword']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Lower Area', 'id': 129, 'game_objects': [{'name': 'Giant Tree 2F - South Box', 'object_id': 173, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'access': ['Claw']}, {'target_room': 131, 'access': ['Claw']}]}, {'name': 'Giant Tree 2F Central Island', 'id': 130, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 135, 'entrance': 282, 'teleporter': [146, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 2F East Ledge', 'id': 131, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Meteor Chest Room', 'id': 132, 'game_objects': [{'name': 'Giant Tree 2F - Gidrah Chest', 'object_id': 22, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 133, 'entrance': 285, 'teleporter': [148, 0], 'access': []}]}, {'name': 'Giant Tree 2F Mushroom Room', 'id': 133, 'game_objects': [{'name': 'Giant Tree 2F - Mushroom Tunnel West Box', 'object_id': 175, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 2F - Mushroom Tunnel East Box', 'object_id': 176, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 127, 'entrance': 286, 'teleporter': [150, 0], 'access': ['Axe']}, {'target_room': 132, 'entrance': 287, 'teleporter': [151, 0], 'access': ['Axe', 'Gidrah']}]}, {'name': 'Giant Tree 3F Central Island', 'id': 135, 'game_objects': [{'name': 'Giant Tree 3F - Central Island Box', 'object_id': 179, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'entrance': 288, 'teleporter': [152, 0], 'access': []}, {'target_room': 136, 'access': ['Claw']}, {'target_room': 137, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 3F Central Area', 'id': 136, 'game_objects': [{'name': 'Giant Tree 3F - Center North Box', 'object_id': 177, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 3F - Center West Box', 'object_id': 178, 'type': 'Box', 'access': []}], 'links': [{'target_room': 135, 'access': ['Claw']}, {'target_room': 127, 'access': []}, {'target_room': 131, 'access': []}]}, {'name': 'Giant Tree 3F Lower Ledge', 'id': 137, 'game_objects': [], 'links': [{'target_room': 135, 'access': ['DragonClaw']}, {'target_room': 142, 'entrance': 289, 'teleporter': [153, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 3F West Area', 'id': 138, 'game_objects': [{'name': 'Giant Tree 3F - West Side Box', 'object_id': 180, 'type': 'Box', 'access': []}], 'links': [{'target_room': 128, 'access': []}, {'target_room': 210, 'entrance': 290, 'teleporter': [154, 0], 'access': []}]}, {'name': 'Giant Tree 3F Middle Up Island', 'id': 139, 'game_objects': [], 'links': [{'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree 3F West Platform', 'id': 140, 'game_objects': [], 'links': [{'target_room': 139, 'access': ['Claw']}, {'target_room': 141, 'access': ['Claw']}, {'target_room': 128, 'entrance': 291, 'teleporter': [155, 0], 'access': []}]}, {'name': 'Giant Tree 3F North Ledge', 'id': 141, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 292, 'teleporter': [156, 0], 'access': ['Sword']}, {'target_room': 139, 'access': ['Claw']}, {'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree Worm Room Upper Ledge', 'id': 142, 'game_objects': [{'name': 'Giant Tree 3F - Worm Room North Box', 'object_id': 181, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 3F - Worm Room South Box', 'object_id': 182, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 137, 'entrance': 293, 'teleporter': [157, 0], 'access': ['Axe']}, {'target_room': 210, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree Worm Room Lower Ledge', 'id': 210, 'game_objects': [], 'links': [{'target_room': 138, 'entrance': 294, 'teleporter': [158, 0], 'access': []}]}, {'name': 'Giant Tree 4F Lower Floor', 'id': 143, 'game_objects': [], 'links': [{'target_room': 141, 'entrance': 295, 'teleporter': [159, 0], 'access': []}, {'target_room': 148, 'entrance': 296, 'teleporter': [160, 0], 'access': []}, {'target_room': 148, 'entrance': 297, 'teleporter': [161, 0], 'access': []}, {'target_room': 147, 'entrance': 298, 'teleporter': [162, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 4F Middle Floor', 'id': 144, 'game_objects': [{'name': 'Giant Tree 4F - Highest Platform North Box', 'object_id': 183, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Highest Platform South Box', 'object_id': 184, 'type': 'Box', 'access': []}], 'links': [{'target_room': 149, 'entrance': 299, 'teleporter': [163, 0], 'access': []}, {'target_room': 145, 'access': ['Claw']}, {'target_room': 146, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Upper Floor', 'id': 145, 'game_objects': [], 'links': [{'target_room': 150, 'entrance': 300, 'teleporter': [164, 0], 'access': ['Sword']}, {'target_room': 144, 'access': ['Claw']}]}, {'name': 'Giant Tree 4F South Ledge', 'id': 146, 'game_objects': [{'name': 'Giant Tree 4F - Hook Ledge Northeast Box', 'object_id': 185, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Hook Ledge Southwest Box', 'object_id': 186, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Slime Room East Area', 'id': 147, 'game_objects': [{'name': 'Giant Tree 4F - East Slime Room Box', 'object_id': 188, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 143, 'entrance': 304, 'teleporter': [168, 0], 'access': []}]}, {'name': 'Giant Tree 4F Slime Room West Area', 'id': 148, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 303, 'teleporter': [167, 0], 'access': ['Axe']}, {'target_room': 143, 'entrance': 302, 'teleporter': [166, 0], 'access': ['Axe']}, {'target_room': 149, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree 4F Slime Room Platform', 'id': 149, 'game_objects': [{'name': 'Giant Tree 4F - West Slime Room Box', 'object_id': 187, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'entrance': 301, 'teleporter': [165, 0], 'access': []}, {'target_room': 148, 'access': ['Claw']}]}, {'name': 'Giant Tree 5F Lower Area', 'id': 150, 'game_objects': [{'name': 'Giant Tree 5F - Northwest Left Box', 'object_id': 189, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - Northwest Right Box', 'object_id': 190, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Left Box', 'object_id': 191, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Right Box', 'object_id': 192, 'type': 'Box', 'access': []}], 'links': [{'target_room': 145, 'entrance': 305, 'teleporter': [169, 0], 'access': []}, {'target_room': 151, 'access': ['Claw']}, {'target_room': 143, 'access': []}]}, {'name': 'Giant Tree 5F Gidrah Platform', 'id': 151, 'game_objects': [{'name': 'Gidrah', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Gidrah'], 'access': []}], 'links': [{'target_room': 150, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Lower Ledge', 'id': 152, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 307, 'teleporter': [18, 6], 'access': []}, {'target_room': 153, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Upper Ledge', 'id': 153, 'game_objects': [{'name': 'Kaidge Temple - Box', 'object_id': 193, 'type': 'Box', 'access': []}], 'links': [{'target_room': 185, 'entrance': 308, 'teleporter': [71, 8], 'access': ['MobiusCrest']}, {'target_room': 152, 'access': ['Claw']}]}, {'name': 'Windhole Temple', 'id': 154, 'game_objects': [{'name': 'Windhole Temple - Box', 'object_id': 194, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 309, 'teleporter': [173, 0], 'access': []}]}, {'name': 'Mount Gale', 'id': 155, 'game_objects': [{'name': 'Mount Gale - Dullahan Chest', 'object_id': 23, 'type': 'Chest', 'access': ['DragonClaw', 'Dullahan']}, {'name': 'Dullahan', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Dullahan'], 'access': ['DragonClaw']}, {'name': 'Mount Gale - East Box', 'object_id': 195, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Mount Gale - West Box', 'object_id': 196, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 310, 'teleporter': [174, 0], 'access': []}]}, {'name': 'Windia', 'id': 156, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 312, 'teleporter': [10, 6], 'access': []}, {'target_room': 157, 'entrance': 320, 'teleporter': [30, 5], 'access': []}, {'target_room': 163, 'entrance': 321, 'teleporter': [97, 8], 'access': []}, {'target_room': 165, 'entrance': 322, 'teleporter': [32, 5], 'access': []}, {'target_room': 159, 'entrance': 323, 'teleporter': [176, 4], 'access': []}, {'target_room': 160, 'entrance': 324, 'teleporter': [177, 4], 'access': []}]}, {'name': "Otto's House", 'id': 157, 'game_objects': [{'name': 'Otto', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['RainbowBridge'], 'access': ['ThunderRock']}], 'links': [{'target_room': 156, 'entrance': 327, 'teleporter': [106, 3], 'access': []}, {'target_room': 158, 'entrance': 326, 'teleporter': [33, 2], 'access': []}]}, {'name': "Otto's Attic", 'id': 158, 'game_objects': [{'name': "Windia - Otto's Attic Box", 'object_id': 197, 'type': 'Box', 'access': []}], 'links': [{'target_room': 157, 'entrance': 328, 'teleporter': [107, 3], 'access': []}]}, {'name': 'Windia Kid House', 'id': 159, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 329, 'teleporter': [178, 0], 'access': []}, {'target_room': 161, 'entrance': 330, 'teleporter': [180, 0], 'access': []}]}, {'name': 'Windia Old People House', 'id': 160, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 331, 'teleporter': [179, 0], 'access': []}, {'target_room': 162, 'entrance': 332, 'teleporter': [181, 0], 'access': []}]}, {'name': 'Windia Kid House Basement', 'id': 161, 'game_objects': [], 'links': [{'target_room': 159, 'entrance': 333, 'teleporter': [182, 0], 'access': []}, {'target_room': 79, 'entrance': 334, 'teleporter': [44, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Old People House Basement', 'id': 162, 'game_objects': [{'name': 'Windia - Mobius Basement West Box', 'object_id': 200, 'type': 'Box', 'access': []}, {'name': 'Windia - Mobius Basement East Box', 'object_id': 201, 'type': 'Box', 'access': []}], 'links': [{'target_room': 160, 'entrance': 335, 'teleporter': [183, 0], 'access': []}, {'target_room': 186, 'entrance': 336, 'teleporter': [43, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Inn Lobby', 'id': 163, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 338, 'teleporter': [135, 3], 'access': []}, {'target_room': 164, 'entrance': 337, 'teleporter': [102, 8], 'access': []}]}, {'name': 'Windia Inn Beds', 'id': 164, 'game_objects': [{'name': 'Windia - Inn Bedroom North Box', 'object_id': 198, 'type': 'Box', 'access': []}, {'name': 'Windia - Inn Bedroom South Box', 'object_id': 199, 'type': 'Box', 'access': []}, {'name': 'Windia - Kaeli', 'object_id': 15, 'type': 'NPC', 'access': ['Kaeli2']}], 'links': [{'target_room': 163, 'entrance': 339, 'teleporter': [216, 0], 'access': []}]}, {'name': 'Windia Vendor House', 'id': 165, 'game_objects': [{'name': 'Windia - Vendor', 'object_id': 16, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 156, 'entrance': 340, 'teleporter': [108, 3], 'access': []}]}, {'name': 'Pazuzu Tower 1F Main Lobby', 'id': 166, 'game_objects': [{'name': 'Pazuzu 1F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu1F'], 'access': []}], 'links': [{'target_room': 226, 'entrance': 341, 'teleporter': [184, 0], 'access': []}, {'target_room': 180, 'entrance': 345, 'teleporter': [185, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Boxes Room', 'id': 167, 'game_objects': [{'name': "Pazuzu's Tower 1F - Descent Bomb Wall West Box", 'object_id': 202, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall Center Box", 'object_id': 203, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall East Box", 'object_id': 204, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Box", 'object_id': 205, 'type': 'Box', 'access': []}], 'links': [{'target_room': 169, 'entrance': 349, 'teleporter': [187, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Southern Platform', 'id': 168, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 346, 'teleporter': [186, 0], 'access': []}, {'target_room': 166, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 2F', 'id': 169, 'game_objects': [{'name': "Pazuzu's Tower 2F - East Room West Box", 'object_id': 206, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 2F - East Room East Box", 'object_id': 207, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 2F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 2F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 350, 'teleporter': [188, 0], 'access': []}, {'target_room': 168, 'entrance': 351, 'teleporter': [189, 0], 'access': []}, {'target_room': 167, 'entrance': 352, 'teleporter': [190, 0], 'access': []}, {'target_room': 171, 'entrance': 353, 'teleporter': [191, 0], 'access': []}]}, {'name': 'Pazuzu 3F Main Room', 'id': 170, 'game_objects': [{'name': "Pazuzu's Tower 3F - Guest Room West Box", 'object_id': 208, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 3F - Guest Room East Box", 'object_id': 209, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 3F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu3F'], 'access': []}], 'links': [{'target_room': 180, 'entrance': 356, 'teleporter': [192, 0], 'access': []}, {'target_room': 181, 'entrance': 357, 'teleporter': [193, 0], 'access': []}]}, {'name': 'Pazuzu 3F Central Island', 'id': 171, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 360, 'teleporter': [194, 0], 'access': []}, {'target_room': 170, 'access': ['DragonClaw']}, {'target_room': 172, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 3F Southern Island', 'id': 172, 'game_objects': [{'name': "Pazuzu's Tower 3F - South Ledge Box", 'object_id': 210, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 361, 'teleporter': [195, 0], 'access': []}, {'target_room': 171, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 4F', 'id': 173, 'game_objects': [{'name': "Pazuzu's Tower 4F - Elevator West Box", 'object_id': 211, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - Elevator East Box", 'object_id': 212, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - East Storage Room Chest", 'object_id': 24, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 4F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 4F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 362, 'teleporter': [196, 0], 'access': []}, {'target_room': 184, 'entrance': 363, 'teleporter': [197, 0], 'access': []}, {'target_room': 172, 'entrance': 364, 'teleporter': [198, 0], 'access': []}, {'target_room': 175, 'entrance': 365, 'teleporter': [199, 0], 'access': []}]}, {'name': 'Pazuzu 5F Pazuzu Loop', 'id': 174, 'game_objects': [{'name': 'Pazuzu 5F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu5F'], 'access': []}], 'links': [{'target_room': 181, 'entrance': 368, 'teleporter': [200, 0], 'access': []}, {'target_room': 182, 'entrance': 369, 'teleporter': [201, 0], 'access': []}]}, {'name': 'Pazuzu 5F Upper Loop', 'id': 175, 'game_objects': [{'name': "Pazuzu's Tower 5F - North Box", 'object_id': 213, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 5F - South Box", 'object_id': 214, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 370, 'teleporter': [202, 0], 'access': []}, {'target_room': 176, 'entrance': 371, 'teleporter': [203, 0], 'access': []}]}, {'name': 'Pazuzu 6F', 'id': 176, 'game_objects': [{'name': "Pazuzu's Tower 6F - Box", 'object_id': 215, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 6F - Chest", 'object_id': 25, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 6F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6FLock'], 'access': ['Bomb', 'Axe']}, {'name': 'Pazuzu 6F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6F'], 'access': ['Bomb']}], 'links': [{'target_room': 184, 'entrance': 374, 'teleporter': [204, 0], 'access': []}, {'target_room': 175, 'entrance': 375, 'teleporter': [205, 0], 'access': []}, {'target_room': 178, 'entrance': 376, 'teleporter': [206, 0], 'access': []}, {'target_room': 178, 'entrance': 377, 'teleporter': [207, 0], 'access': []}]}, {'name': 'Pazuzu 7F Southwest Area', 'id': 177, 'game_objects': [], 'links': [{'target_room': 182, 'entrance': 380, 'teleporter': [26, 0], 'access': []}, {'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 7F Rest of the Area', 'id': 178, 'game_objects': [], 'links': [{'target_room': 177, 'access': ['DragonClaw']}, {'target_room': 176, 'entrance': 381, 'teleporter': [27, 0], 'access': []}, {'target_room': 176, 'entrance': 382, 'teleporter': [28, 0], 'access': []}, {'target_room': 179, 'access': ['DragonClaw', 'Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}]}, {'name': 'Pazuzu 7F Sky Room', 'id': 179, 'game_objects': [{'name': "Pazuzu's Tower 7F - Pazuzu Chest", 'object_id': 26, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu'], 'access': ['Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}], 'links': [{'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 1F to 3F', 'id': 180, 'game_objects': [], 'links': [{'target_room': 166, 'entrance': 385, 'teleporter': [29, 0], 'access': []}, {'target_room': 170, 'entrance': 386, 'teleporter': [30, 0], 'access': []}]}, {'name': 'Pazuzu 3F to 5F', 'id': 181, 'game_objects': [], 'links': [{'target_room': 170, 'entrance': 387, 'teleporter': [40, 0], 'access': []}, {'target_room': 174, 'entrance': 388, 'teleporter': [41, 0], 'access': []}]}, {'name': 'Pazuzu 5F to 7F', 'id': 182, 'game_objects': [], 'links': [{'target_room': 174, 'entrance': 389, 'teleporter': [38, 0], 'access': []}, {'target_room': 177, 'entrance': 390, 'teleporter': [39, 0], 'access': []}]}, {'name': 'Pazuzu 2F to 4F', 'id': 183, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 391, 'teleporter': [21, 0], 'access': []}, {'target_room': 173, 'entrance': 392, 'teleporter': [22, 0], 'access': []}]}, {'name': 'Pazuzu 4F to 6F', 'id': 184, 'game_objects': [], 'links': [{'target_room': 173, 'entrance': 393, 'teleporter': [2, 0], 'access': []}, {'target_room': 176, 'entrance': 394, 'teleporter': [3, 0], 'access': []}]}, {'name': 'Light Temple', 'id': 185, 'game_objects': [{'name': 'Light Temple - Box', 'object_id': 216, 'type': 'Box', 'access': []}], 'links': [{'target_room': 230, 'entrance': 395, 'teleporter': [19, 6], 'access': []}, {'target_room': 153, 'entrance': 396, 'teleporter': [70, 8], 'access': ['MobiusCrest']}]}, {'name': 'Ship Dock', 'id': 186, 'game_objects': [{'name': 'Ship Dock Access', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipDockAccess'], 'access': []}], 'links': [{'target_room': 228, 'entrance': 399, 'teleporter': [17, 6], 'access': []}, {'target_room': 162, 'entrance': 397, 'teleporter': [61, 8], 'access': ['MobiusCrest']}]}, {'name': 'Mac Ship Deck', 'id': 187, 'game_objects': [{'name': 'Mac Ship Steering Wheel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipSteeringWheel'], 'access': []}, {'name': "Mac's Ship Deck - North Box", 'object_id': 217, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - Center Box", 'object_id': 218, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - South Box", 'object_id': 219, 'type': 'Box', 'access': []}], 'links': [{'target_room': 229, 'entrance': 400, 'teleporter': [37, 8], 'access': []}, {'target_room': 188, 'entrance': 401, 'teleporter': [50, 8], 'access': []}, {'target_room': 188, 'entrance': 402, 'teleporter': [51, 8], 'access': []}, {'target_room': 188, 'entrance': 403, 'teleporter': [52, 8], 'access': []}, {'target_room': 189, 'entrance': 404, 'teleporter': [53, 8], 'access': []}]}, {'name': 'Mac Ship B1 Outer Ring', 'id': 188, 'game_objects': [{'name': "Mac's Ship B1 - Northwest Hook Platform Box", 'object_id': 228, 'type': 'Box', 'access': ['DragonClaw']}, {'name': "Mac's Ship B1 - Center Hook Platform Box", 'object_id': 229, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 187, 'entrance': 405, 'teleporter': [208, 0], 'access': []}, {'target_room': 187, 'entrance': 406, 'teleporter': [175, 0], 'access': []}, {'target_room': 187, 'entrance': 407, 'teleporter': [172, 0], 'access': []}, {'target_room': 193, 'entrance': 408, 'teleporter': [88, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B1 Square Room', 'id': 189, 'game_objects': [], 'links': [{'target_room': 187, 'entrance': 409, 'teleporter': [141, 0], 'access': []}, {'target_room': 192, 'entrance': 410, 'teleporter': [87, 0], 'access': []}]}, {'name': 'Mac Ship B1 Central Corridor', 'id': 190, 'game_objects': [{'name': "Mac's Ship B1 - Central Corridor Box", 'object_id': 230, 'type': 'Box', 'access': []}], 'links': [{'target_room': 192, 'entrance': 413, 'teleporter': [86, 0], 'access': []}, {'target_room': 191, 'entrance': 412, 'teleporter': [102, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B2 South Corridor', 'id': 191, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 415, 'teleporter': [55, 8], 'access': []}, {'target_room': 194, 'entrance': 414, 'teleporter': [57, 1], 'access': []}]}, {'name': 'Mac Ship B2 North Corridor', 'id': 192, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 416, 'teleporter': [56, 8], 'access': []}, {'target_room': 189, 'entrance': 417, 'teleporter': [57, 8], 'access': []}]}, {'name': 'Mac Ship B2 Outer Ring', 'id': 193, 'game_objects': [{'name': "Mac's Ship B2 - Barrel Room South Box", 'object_id': 223, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Barrel Room North Box", 'object_id': 224, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southwest Room Box", 'object_id': 225, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southeast Room Box", 'object_id': 226, 'type': 'Box', 'access': []}], 'links': [{'target_room': 188, 'entrance': 418, 'teleporter': [58, 8], 'access': []}]}, {'name': 'Mac Ship B1 Mac Room', 'id': 194, 'game_objects': [{'name': "Mac's Ship B1 - Mac Room Chest", 'object_id': 27, 'type': 'Chest', 'access': []}, {'name': 'Captain Mac', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLoaned'], 'access': ['CaptainCap']}], 'links': [{'target_room': 191, 'entrance': 424, 'teleporter': [101, 0], 'access': []}]}, {'name': 'Doom Castle Corridor of Destiny', 'id': 195, 'game_objects': [], 'links': [{'target_room': 201, 'entrance': 428, 'teleporter': [84, 0], 'access': []}, {'target_room': 196, 'entrance': 429, 'teleporter': [35, 2], 'access': []}, {'target_room': 197, 'entrance': 430, 'teleporter': [209, 0], 'access': ['StoneGolem']}, {'target_room': 198, 'entrance': 431, 'teleporter': [211, 0], 'access': ['StoneGolem', 'TwinheadWyvern']}, {'target_room': 199, 'entrance': 432, 'teleporter': [13, 2], 'access': ['StoneGolem', 'TwinheadWyvern', 'Zuh']}]}, {'name': 'Doom Castle Ice Floor', 'id': 196, 'game_objects': [{'name': 'Doom Castle 4F - Northwest Room Box', 'object_id': 231, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Southwest Room Box', 'object_id': 232, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Northeast Room Box', 'object_id': 233, 'type': 'Box', 'access': ['Sword']}, {'name': 'Doom Castle 4F - Southeast Room Box', 'object_id': 234, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Stone Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['StoneGolem'], 'access': ['Sword', 'DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 433, 'teleporter': [109, 3], 'access': []}]}, {'name': 'Doom Castle Lava Floor', 'id': 197, 'game_objects': [{'name': 'Doom Castle 5F - North Left Box', 'object_id': 235, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - North Right Box', 'object_id': 236, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Left Box', 'object_id': 237, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Right Box', 'object_id': 238, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Twinhead Wyvern', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TwinheadWyvern'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 434, 'teleporter': [210, 0], 'access': []}]}, {'name': 'Doom Castle Sky Floor', 'id': 198, 'game_objects': [{'name': 'Doom Castle 6F - West Box', 'object_id': 239, 'type': 'Box', 'access': []}, {'name': 'Doom Castle 6F - East Box', 'object_id': 240, 'type': 'Box', 'access': []}, {'name': 'Zuh', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Zuh'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 435, 'teleporter': [212, 0], 'access': []}, {'target_room': 197, 'access': []}]}, {'name': 'Doom Castle Hero Room', 'id': 199, 'game_objects': [{'name': 'Doom Castle Hero Chest 01', 'object_id': 242, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 02', 'object_id': 243, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 03', 'object_id': 244, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 04', 'object_id': 245, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 200, 'entrance': 436, 'teleporter': [54, 0], 'access': []}, {'target_room': 195, 'entrance': 441, 'teleporter': [110, 3], 'access': []}]}, {'name': 'Doom Castle Dark King Room', 'id': 200, 'game_objects': [], 'links': [{'target_room': 199, 'entrance': 442, 'teleporter': [52, 0], 'access': []}]}] +entrances = [{'name': 'Doom Castle - Sand Floor - To Sky Door - Sand Floor', 'id': 0, 'area': 7, 'coordinates': [24, 19], 'teleporter': [0, 0]}, {'name': 'Doom Castle - Sand Floor - Main Entrance - Sand Floor', 'id': 1, 'area': 7, 'coordinates': [19, 43], 'teleporter': [1, 6]}, {'name': 'Doom Castle - Aero Room - Aero Room Entrance', 'id': 2, 'area': 7, 'coordinates': [27, 39], 'teleporter': [1, 0]}, {'name': 'Focus Tower B1 - Main Loop - South Entrance', 'id': 3, 'area': 8, 'coordinates': [43, 60], 'teleporter': [2, 6]}, {'name': 'Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall', 'id': 4, 'area': 8, 'coordinates': [37, 41], 'teleporter': [4, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room', 'id': 5, 'area': 8, 'coordinates': [59, 35], 'teleporter': [5, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest', 'id': 6, 'area': 8, 'coordinates': [57, 59], 'teleporter': [8, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door', 'id': 7, 'area': 8, 'coordinates': [51, 49], 'teleporter': [6, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor', 'id': 8, 'area': 8, 'coordinates': [51, 45], 'teleporter': [7, 0]}, {'name': 'Focus Tower 1F - Focus Tower West Entrance', 'id': 9, 'area': 9, 'coordinates': [25, 29], 'teleporter': [3, 6]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From SandCoin', 'id': 10, 'area': 9, 'coordinates': [16, 4], 'teleporter': [10, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - Main Hall', 'id': 11, 'area': 9, 'coordinates': [4, 23], 'teleporter': [11, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - To Aero Chest', 'id': 12, 'area': 9, 'coordinates': [26, 17], 'teleporter': [12, 0]}, {'name': 'Focus Tower 1F - Sky Door', 'id': 13, 'area': 9, 'coordinates': [16, 24], 'teleporter': [13, 0]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From RiverCoin', 'id': 14, 'area': 9, 'coordinates': [16, 10], 'teleporter': [14, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - From Sky Door', 'id': 15, 'area': 9, 'coordinates': [16, 29], 'teleporter': [15, 0]}, {'name': 'Focus Tower 2F - Sand Coin Passage - North Entrance', 'id': 16, 'area': 10, 'coordinates': [49, 30], 'teleporter': [4, 6]}, {'name': 'Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin', 'id': 17, 'area': 10, 'coordinates': [47, 33], 'teleporter': [17, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin', 'id': 18, 'area': 10, 'coordinates': [47, 41], 'teleporter': [18, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor', 'id': 19, 'area': 10, 'coordinates': [38, 40], 'teleporter': [20, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor', 'id': 20, 'area': 10, 'coordinates': [56, 40], 'teleporter': [19, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - Pillar Script', 'id': 21, 'area': 10, 'coordinates': [48, 53], 'teleporter': [13, 8]}, {'name': 'Focus Tower 3F - Lower Floor - To Fireburg Entrance', 'id': 22, 'area': 11, 'coordinates': [11, 39], 'teleporter': [6, 6]}, {'name': 'Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar', 'id': 23, 'area': 11, 'coordinates': [6, 47], 'teleporter': [24, 0]}, {'name': 'Focus Tower 3F - Upper Floor - To Aquaria Entrance', 'id': 24, 'area': 11, 'coordinates': [21, 38], 'teleporter': [5, 6]}, {'name': 'Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room', 'id': 25, 'area': 11, 'coordinates': [24, 47], 'teleporter': [23, 0]}, {'name': 'Level Forest - Boulder Script', 'id': 26, 'area': 14, 'coordinates': [52, 15], 'teleporter': [0, 8]}, {'name': 'Level Forest - Rotten Tree Script', 'id': 27, 'area': 14, 'coordinates': [47, 6], 'teleporter': [2, 8]}, {'name': 'Level Forest - Exit Level Forest 1', 'id': 28, 'area': 14, 'coordinates': [46, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 2', 'id': 29, 'area': 14, 'coordinates': [46, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 3', 'id': 30, 'area': 14, 'coordinates': [47, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 4', 'id': 31, 'area': 14, 'coordinates': [47, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 5', 'id': 32, 'area': 14, 'coordinates': [60, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 6', 'id': 33, 'area': 14, 'coordinates': [61, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 7', 'id': 34, 'area': 14, 'coordinates': [46, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 8', 'id': 35, 'area': 14, 'coordinates': [46, 3], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 9', 'id': 36, 'area': 14, 'coordinates': [47, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest A', 'id': 37, 'area': 14, 'coordinates': [47, 3], 'teleporter': [25, 0]}, {'name': 'Foresta - Exit Foresta 1', 'id': 38, 'area': 15, 'coordinates': [10, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 2', 'id': 39, 'area': 15, 'coordinates': [10, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 3', 'id': 40, 'area': 15, 'coordinates': [11, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 4', 'id': 41, 'area': 15, 'coordinates': [11, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Old Man House - Front Door', 'id': 42, 'area': 15, 'coordinates': [25, 17], 'teleporter': [32, 4]}, {'name': 'Foresta - Old Man House - Back Door', 'id': 43, 'area': 15, 'coordinates': [25, 14], 'teleporter': [33, 0]}, {'name': "Foresta - Kaeli's House", 'id': 44, 'area': 15, 'coordinates': [7, 21], 'teleporter': [0, 5]}, {'name': 'Foresta - Rest House', 'id': 45, 'area': 15, 'coordinates': [23, 23], 'teleporter': [1, 5]}, {'name': "Kaeli's House - Kaeli's House Entrance", 'id': 46, 'area': 16, 'coordinates': [11, 20], 'teleporter': [86, 3]}, {'name': "Foresta Houses - Old Man's House - Old Man Front Exit", 'id': 47, 'area': 17, 'coordinates': [35, 44], 'teleporter': [34, 0]}, {'name': "Foresta Houses - Old Man's House - Old Man Back Exit", 'id': 48, 'area': 17, 'coordinates': [35, 27], 'teleporter': [35, 0]}, {'name': 'Foresta - Old Man House - Barrel Tile Script', 'id': 483, 'area': 17, 'coordinates': [35, 30], 'teleporter': [13, 8]}, {'name': 'Foresta Houses - Rest House - Bed Script', 'id': 49, 'area': 17, 'coordinates': [30, 6], 'teleporter': [1, 8]}, {'name': 'Foresta Houses - Rest House - Rest House Exit', 'id': 50, 'area': 17, 'coordinates': [35, 20], 'teleporter': [87, 3]}, {'name': 'Foresta Houses - Libra House - Libra House Script', 'id': 51, 'area': 17, 'coordinates': [8, 49], 'teleporter': [67, 8]}, {'name': 'Foresta Houses - Gemini House - Gemini House Script', 'id': 52, 'area': 17, 'coordinates': [26, 55], 'teleporter': [68, 8]}, {'name': 'Foresta Houses - Mobius House - Mobius House Script', 'id': 53, 'area': 17, 'coordinates': [14, 33], 'teleporter': [69, 8]}, {'name': 'Sand Temple - Sand Temple Entrance', 'id': 54, 'area': 18, 'coordinates': [56, 27], 'teleporter': [36, 0]}, {'name': 'Bone Dungeon 1F - Bone Dungeon Entrance', 'id': 55, 'area': 19, 'coordinates': [13, 60], 'teleporter': [37, 0]}, {'name': 'Bone Dungeon 1F - To Bone Dungeon B1', 'id': 56, 'area': 19, 'coordinates': [13, 39], 'teleporter': [2, 2]}, {'name': 'Bone Dungeon B1 - Waterway - Exit Waterway', 'id': 57, 'area': 20, 'coordinates': [27, 39], 'teleporter': [3, 2]}, {'name': "Bone Dungeon B1 - Waterway - Tristam's Script", 'id': 58, 'area': 20, 'coordinates': [27, 45], 'teleporter': [3, 8]}, {'name': 'Bone Dungeon B1 - Waterway - To Bone Dungeon 1F', 'id': 59, 'area': 20, 'coordinates': [54, 61], 'teleporter': [88, 3]}, {'name': 'Bone Dungeon B1 - Checker Room - Exit Checker Room', 'id': 60, 'area': 20, 'coordinates': [23, 40], 'teleporter': [4, 2]}, {'name': 'Bone Dungeon B1 - Checker Room - To Waterway', 'id': 61, 'area': 20, 'coordinates': [39, 49], 'teleporter': [89, 3]}, {'name': 'Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room', 'id': 62, 'area': 20, 'coordinates': [5, 33], 'teleporter': [91, 3]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage', 'id': 63, 'area': 21, 'coordinates': [19, 13], 'teleporter': [5, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room', 'id': 64, 'area': 21, 'coordinates': [29, 15], 'teleporter': [6, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Checker Room', 'id': 65, 'area': 21, 'coordinates': [8, 25], 'teleporter': [90, 3]}, {'name': 'Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room', 'id': 66, 'area': 21, 'coordinates': [59, 12], 'teleporter': [93, 3]}, {'name': 'Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room', 'id': 67, 'area': 21, 'coordinates': [59, 28], 'teleporter': [94, 3]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Box Room', 'id': 68, 'area': 21, 'coordinates': [53, 7], 'teleporter': [7, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Quake Room', 'id': 69, 'area': 21, 'coordinates': [41, 3], 'teleporter': [8, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Boss Room', 'id': 70, 'area': 21, 'coordinates': [47, 57], 'teleporter': [9, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room', 'id': 71, 'area': 21, 'coordinates': [54, 23], 'teleporter': [92, 3]}, {'name': 'Bone Dungeon B2 - Boss Room - Flamerus Rex Script', 'id': 72, 'area': 22, 'coordinates': [29, 19], 'teleporter': [4, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - Tristam Leave Script', 'id': 73, 'area': 22, 'coordinates': [29, 23], 'teleporter': [75, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room', 'id': 74, 'area': 22, 'coordinates': [30, 27], 'teleporter': [95, 3]}, {'name': 'Libra Temple - Entrance', 'id': 75, 'area': 23, 'coordinates': [10, 15], 'teleporter': [13, 6]}, {'name': 'Libra Temple - Libra Tile Script', 'id': 76, 'area': 23, 'coordinates': [9, 8], 'teleporter': [59, 8]}, {'name': 'Aquaria Winter - Winter Entrance 1', 'id': 77, 'area': 24, 'coordinates': [25, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 2', 'id': 78, 'area': 24, 'coordinates': [25, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 3', 'id': 79, 'area': 24, 'coordinates': [26, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 4', 'id': 80, 'area': 24, 'coordinates': [26, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Winter - Winter Phoebe's House Entrance Script", 'id': 81, 'area': 24, 'coordinates': [8, 19], 'teleporter': [10, 5]}, {'name': 'Aquaria Winter - Winter Vendor House Entrance', 'id': 82, 'area': 24, 'coordinates': [8, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Winter - Winter INN Entrance', 'id': 83, 'area': 24, 'coordinates': [26, 17], 'teleporter': [11, 5]}, {'name': 'Aquaria Summer - Summer Entrance 1', 'id': 84, 'area': 25, 'coordinates': [57, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 2', 'id': 85, 'area': 25, 'coordinates': [57, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 3', 'id': 86, 'area': 25, 'coordinates': [58, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 4', 'id': 87, 'area': 25, 'coordinates': [58, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Summer - Summer Phoebe's House Entrance", 'id': 88, 'area': 25, 'coordinates': [40, 19], 'teleporter': [10, 5]}, {'name': "Aquaria Summer - Spencer's Place Entrance Top", 'id': 89, 'area': 25, 'coordinates': [40, 16], 'teleporter': [42, 0]}, {'name': "Aquaria Summer - Spencer's Place Entrance Side", 'id': 90, 'area': 25, 'coordinates': [41, 18], 'teleporter': [43, 0]}, {'name': 'Aquaria Summer - Summer Vendor House Entrance', 'id': 91, 'area': 25, 'coordinates': [40, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Summer - Summer INN Entrance', 'id': 92, 'area': 25, 'coordinates': [58, 17], 'teleporter': [11, 5]}, {'name': "Phoebe's House - Entrance", 'id': 93, 'area': 26, 'coordinates': [29, 14], 'teleporter': [5, 8]}, {'name': "Aquaria Vendor House - Vendor House Entrance's Script", 'id': 94, 'area': 27, 'coordinates': [7, 10], 'teleporter': [40, 8]}, {'name': 'Aquaria Vendor House - Vendor House Stairs', 'id': 95, 'area': 27, 'coordinates': [1, 4], 'teleporter': [47, 0]}, {'name': 'Aquaria Gemini Room - Gemini Script', 'id': 96, 'area': 27, 'coordinates': [2, 40], 'teleporter': [72, 8]}, {'name': 'Aquaria Gemini Room - Gemini Room Stairs', 'id': 97, 'area': 27, 'coordinates': [4, 39], 'teleporter': [48, 0]}, {'name': 'Aquaria INN - Aquaria INN entrance', 'id': 98, 'area': 27, 'coordinates': [51, 46], 'teleporter': [75, 8]}, {'name': 'Wintry Cave 1F - Main Entrance', 'id': 99, 'area': 28, 'coordinates': [50, 58], 'teleporter': [49, 0]}, {'name': 'Wintry Cave 1F - To 3F Top', 'id': 100, 'area': 28, 'coordinates': [40, 25], 'teleporter': [14, 2]}, {'name': 'Wintry Cave 1F - To 2F', 'id': 101, 'area': 28, 'coordinates': [10, 43], 'teleporter': [15, 2]}, {'name': "Wintry Cave 1F - Phoebe's Script", 'id': 102, 'area': 28, 'coordinates': [44, 37], 'teleporter': [6, 8]}, {'name': 'Wintry Cave 2F - To 3F Bottom', 'id': 103, 'area': 29, 'coordinates': [58, 5], 'teleporter': [50, 0]}, {'name': 'Wintry Cave 2F - To 1F', 'id': 104, 'area': 29, 'coordinates': [38, 18], 'teleporter': [97, 3]}, {'name': 'Wintry Cave 3F Top - Exit from 3F Top', 'id': 105, 'area': 30, 'coordinates': [24, 6], 'teleporter': [96, 3]}, {'name': 'Wintry Cave 3F Bottom - Exit to 2F', 'id': 106, 'area': 31, 'coordinates': [4, 29], 'teleporter': [51, 0]}, {'name': 'Life Temple - Entrance', 'id': 107, 'area': 32, 'coordinates': [9, 60], 'teleporter': [14, 6]}, {'name': 'Life Temple - Libra Tile Script', 'id': 108, 'area': 32, 'coordinates': [3, 55], 'teleporter': [60, 8]}, {'name': 'Life Temple - Mysterious Man Script', 'id': 109, 'area': 32, 'coordinates': [9, 44], 'teleporter': [78, 8]}, {'name': 'Fall Basin - Back Exit Script', 'id': 110, 'area': 33, 'coordinates': [17, 5], 'teleporter': [9, 0]}, {'name': 'Fall Basin - Main Exit', 'id': 111, 'area': 33, 'coordinates': [15, 26], 'teleporter': [53, 0]}, {'name': "Fall Basin - Phoebe's Script", 'id': 112, 'area': 33, 'coordinates': [17, 6], 'teleporter': [9, 8]}, {'name': 'Ice Pyramid B1 Taunt Room - To Climbing Wall Room', 'id': 113, 'area': 34, 'coordinates': [43, 6], 'teleporter': [55, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 1', 'id': 114, 'area': 35, 'coordinates': [18, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 2', 'id': 115, 'area': 35, 'coordinates': [19, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room', 'id': 116, 'area': 35, 'coordinates': [3, 27], 'teleporter': [57, 0]}, {'name': 'Ice Pyramid 1F Maze - West Center Stairs to 2F West Room', 'id': 117, 'area': 35, 'coordinates': [11, 15], 'teleporter': [58, 0]}, {'name': 'Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room', 'id': 118, 'area': 35, 'coordinates': [25, 16], 'teleporter': [59, 0]}, {'name': 'Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room', 'id': 119, 'area': 35, 'coordinates': [31, 1], 'teleporter': [60, 0]}, {'name': 'Ice Pyramid 1F Maze - East Stairs to 2F North Corridor', 'id': 120, 'area': 35, 'coordinates': [34, 9], 'teleporter': [61, 0]}, {'name': "Ice Pyramid 1F Maze - Statue's Script", 'id': 121, 'area': 35, 'coordinates': [21, 32], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 1F', 'id': 122, 'area': 36, 'coordinates': [4, 26], 'teleporter': [62, 0]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room', 'id': 123, 'area': 36, 'coordinates': [22, 17], 'teleporter': [67, 0]}, {'name': 'Ice Pyramid 2F West Room - To 1F', 'id': 124, 'area': 36, 'coordinates': [9, 10], 'teleporter': [63, 0]}, {'name': 'Ice Pyramid 2F Center Room - To 1F', 'id': 125, 'area': 36, 'coordinates': [22, 14], 'teleporter': [64, 0]}, {'name': 'Ice Pyramid 2F Small North Room - To 1F', 'id': 126, 'area': 36, 'coordinates': [26, 4], 'teleporter': [65, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 1F', 'id': 127, 'area': 36, 'coordinates': [32, 8], 'teleporter': [66, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 3F Main Loop', 'id': 128, 'area': 36, 'coordinates': [12, 7], 'teleporter': [68, 0]}, {'name': 'Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room', 'id': 129, 'area': 37, 'coordinates': [24, 54], 'teleporter': [69, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 2F Corridor', 'id': 130, 'area': 37, 'coordinates': [16, 45], 'teleporter': [70, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 4F', 'id': 131, 'area': 37, 'coordinates': [19, 43], 'teleporter': [71, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 3F Main Loop', 'id': 132, 'area': 38, 'coordinates': [52, 5], 'teleporter': [72, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room', 'id': 133, 'area': 38, 'coordinates': [62, 19], 'teleporter': [73, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room', 'id': 134, 'area': 39, 'coordinates': [54, 63], 'teleporter': [74, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate', 'id': 135, 'area': 39, 'coordinates': [47, 54], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room', 'id': 136, 'area': 39, 'coordinates': [39, 43], 'teleporter': [75, 0]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room', 'id': 137, 'area': 39, 'coordinates': [39, 60], 'teleporter': [76, 0]}, {'name': 'Ice Pyramid - Duplicate Ice Golem Room', 'id': 138, 'area': 40, 'coordinates': [44, 43], 'teleporter': [77, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To Taunt Room', 'id': 139, 'area': 41, 'coordinates': [4, 59], 'teleporter': [78, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To 5F Stairs', 'id': 140, 'area': 41, 'coordinates': [4, 45], 'teleporter': [79, 0]}, {'name': 'Ice Pyramid Ice Golem Room - To 5F Stairs', 'id': 141, 'area': 42, 'coordinates': [44, 43], 'teleporter': [80, 0]}, {'name': 'Ice Pyramid Ice Golem Room - Ice Golem Script', 'id': 142, 'area': 42, 'coordinates': [53, 32], 'teleporter': [10, 8]}, {'name': 'Spencer Waterfall - To Spencer Cave', 'id': 143, 'area': 43, 'coordinates': [48, 57], 'teleporter': [81, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 1', 'id': 144, 'area': 43, 'coordinates': [40, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 2', 'id': 145, 'area': 43, 'coordinates': [40, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 3', 'id': 146, 'area': 43, 'coordinates': [41, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 4', 'id': 147, 'area': 43, 'coordinates': [41, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 1', 'id': 148, 'area': 43, 'coordinates': [46, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 2', 'id': 149, 'area': 43, 'coordinates': [47, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Cave Normal Main - To Waterfall', 'id': 150, 'area': 44, 'coordinates': [14, 39], 'teleporter': [85, 0]}, {'name': 'Spencer Cave Normal From Overworld - Exit to Overworld', 'id': 151, 'area': 44, 'coordinates': [15, 57], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Exit to Overworld', 'id': 152, 'area': 45, 'coordinates': [40, 29], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Libra Teleporter Start Script', 'id': 153, 'area': 45, 'coordinates': [28, 21], 'teleporter': [33, 8]}, {'name': 'Spencer Cave Unplug - Libra Teleporter End Script', 'id': 154, 'area': 45, 'coordinates': [46, 4], 'teleporter': [34, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Chest Script', 'id': 155, 'area': 45, 'coordinates': [21, 9], 'teleporter': [35, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Start Script', 'id': 156, 'area': 45, 'coordinates': [29, 28], 'teleporter': [36, 8]}, {'name': 'Wintry Temple Outer Room - Main Entrance', 'id': 157, 'area': 46, 'coordinates': [8, 31], 'teleporter': [15, 6]}, {'name': 'Wintry Temple Inner Room - Gemini Tile to Sealed temple', 'id': 158, 'area': 46, 'coordinates': [9, 24], 'teleporter': [62, 8]}, {'name': 'Fireburg - To Overworld', 'id': 159, 'area': 47, 'coordinates': [4, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 160, 'area': 47, 'coordinates': [5, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 161, 'area': 47, 'coordinates': [28, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 162, 'area': 47, 'coordinates': [27, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - Vendor House', 'id': 163, 'area': 47, 'coordinates': [10, 24], 'teleporter': [91, 0]}, {'name': 'Fireburg - Reuben House', 'id': 164, 'area': 47, 'coordinates': [14, 6], 'teleporter': [98, 8]}, {'name': 'Fireburg - Hotel', 'id': 165, 'area': 47, 'coordinates': [20, 8], 'teleporter': [96, 8]}, {'name': 'Fireburg - GrenadeMan House Script', 'id': 166, 'area': 47, 'coordinates': [12, 18], 'teleporter': [11, 8]}, {'name': 'Reuben House - Main Entrance', 'id': 167, 'area': 48, 'coordinates': [33, 46], 'teleporter': [98, 3]}, {'name': 'GrenadeMan House - Entrance Script', 'id': 168, 'area': 49, 'coordinates': [55, 60], 'teleporter': [9, 8]}, {'name': 'GrenadeMan House - To Mobius Crest Room', 'id': 169, 'area': 49, 'coordinates': [57, 52], 'teleporter': [93, 0]}, {'name': 'GrenadeMan Mobius Room - Stairs to House', 'id': 170, 'area': 49, 'coordinates': [39, 26], 'teleporter': [94, 0]}, {'name': 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'id': 171, 'area': 49, 'coordinates': [39, 23], 'teleporter': [54, 8]}, {'name': 'Fireburg Vendor House - Entrance Script', 'id': 172, 'area': 49, 'coordinates': [7, 10], 'teleporter': [95, 0]}, {'name': 'Fireburg Vendor House - Stairs to Gemini Room', 'id': 173, 'area': 49, 'coordinates': [1, 4], 'teleporter': [96, 0]}, {'name': 'Fireburg Gemini Room - Stairs to Vendor House', 'id': 174, 'area': 49, 'coordinates': [4, 39], 'teleporter': [97, 0]}, {'name': 'Fireburg Gemini Room - Gemini Teleporter Script', 'id': 175, 'area': 49, 'coordinates': [2, 40], 'teleporter': [45, 8]}, {'name': 'Fireburg Hotel Lobby - Stairs to beds', 'id': 176, 'area': 49, 'coordinates': [4, 50], 'teleporter': [213, 0]}, {'name': 'Fireburg Hotel Lobby - Entrance', 'id': 177, 'area': 49, 'coordinates': [17, 56], 'teleporter': [99, 3]}, {'name': 'Fireburg Hotel Beds - Stairs to Hotel Lobby', 'id': 178, 'area': 49, 'coordinates': [45, 59], 'teleporter': [214, 0]}, {'name': 'Mine Exterior - Main Entrance', 'id': 179, 'area': 50, 'coordinates': [5, 28], 'teleporter': [98, 0]}, {'name': 'Mine Exterior - To Cliff', 'id': 180, 'area': 50, 'coordinates': [58, 29], 'teleporter': [99, 0]}, {'name': 'Mine Exterior - To Parallel Room', 'id': 181, 'area': 50, 'coordinates': [8, 7], 'teleporter': [20, 2]}, {'name': 'Mine Exterior - To Crescent Room', 'id': 182, 'area': 50, 'coordinates': [26, 15], 'teleporter': [21, 2]}, {'name': 'Mine Exterior - To Climbing Room', 'id': 183, 'area': 50, 'coordinates': [21, 35], 'teleporter': [22, 2]}, {'name': 'Mine Exterior - Jinn Fight Script', 'id': 184, 'area': 50, 'coordinates': [58, 31], 'teleporter': [74, 8]}, {'name': 'Mine Parallel Room - To Mine Exterior', 'id': 185, 'area': 51, 'coordinates': [7, 60], 'teleporter': [100, 3]}, {'name': 'Mine Crescent Room - To Mine Exterior', 'id': 186, 'area': 51, 'coordinates': [22, 61], 'teleporter': [101, 3]}, {'name': 'Mine Climbing Room - To Mine Exterior', 'id': 187, 'area': 51, 'coordinates': [56, 21], 'teleporter': [102, 3]}, {'name': 'Mine Cliff - Entrance', 'id': 188, 'area': 52, 'coordinates': [9, 5], 'teleporter': [100, 0]}, {'name': 'Mine Cliff - Reuben Grenade Script', 'id': 189, 'area': 52, 'coordinates': [15, 7], 'teleporter': [12, 8]}, {'name': 'Sealed Temple - To Overworld', 'id': 190, 'area': 53, 'coordinates': [58, 43], 'teleporter': [16, 6]}, {'name': 'Sealed Temple - Gemini Tile Script', 'id': 191, 'area': 53, 'coordinates': [56, 38], 'teleporter': [63, 8]}, {'name': 'Volcano Base - Main Entrance 1', 'id': 192, 'area': 54, 'coordinates': [23, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 2', 'id': 193, 'area': 54, 'coordinates': [23, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 3', 'id': 194, 'area': 54, 'coordinates': [24, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 4', 'id': 195, 'area': 54, 'coordinates': [24, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Left Stairs Script', 'id': 196, 'area': 54, 'coordinates': [20, 5], 'teleporter': [31, 8]}, {'name': 'Volcano Base - Right Stairs Script', 'id': 197, 'area': 54, 'coordinates': [32, 5], 'teleporter': [30, 8]}, {'name': 'Volcano Top Right - Top Exit', 'id': 198, 'area': 55, 'coordinates': [44, 8], 'teleporter': [9, 0]}, {'name': 'Volcano Top Left - To Right-Left Path Script', 'id': 199, 'area': 55, 'coordinates': [40, 24], 'teleporter': [26, 8]}, {'name': 'Volcano Top Right - To Left-Right Path Script', 'id': 200, 'area': 55, 'coordinates': [52, 24], 'teleporter': [79, 8]}, {'name': 'Volcano Right Path - To Volcano Base Script', 'id': 201, 'area': 56, 'coordinates': [48, 42], 'teleporter': [15, 8]}, {'name': 'Volcano Left Path - To Volcano Cross Left-Right', 'id': 202, 'area': 56, 'coordinates': [40, 31], 'teleporter': [25, 2]}, {'name': 'Volcano Left Path - To Volcano Cross Right-Left', 'id': 203, 'area': 56, 'coordinates': [52, 29], 'teleporter': [26, 2]}, {'name': 'Volcano Left Path - To Volcano Base Script', 'id': 204, 'area': 56, 'coordinates': [36, 42], 'teleporter': [27, 8]}, {'name': 'Volcano Cross Left-Right - To Volcano Left Path', 'id': 205, 'area': 56, 'coordinates': [10, 42], 'teleporter': [103, 3]}, {'name': 'Volcano Cross Left-Right - To Volcano Top Right Script', 'id': 206, 'area': 56, 'coordinates': [16, 24], 'teleporter': [29, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Top Left Script', 'id': 207, 'area': 56, 'coordinates': [8, 22], 'teleporter': [28, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Left Path', 'id': 208, 'area': 56, 'coordinates': [16, 42], 'teleporter': [104, 3]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 1', 'id': 209, 'area': 57, 'coordinates': [32, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 2', 'id': 210, 'area': 57, 'coordinates': [33, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Three Steps Room', 'id': 211, 'area': 57, 'coordinates': [14, 5], 'teleporter': [105, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Life Chest Room Lower', 'id': 212, 'area': 57, 'coordinates': [40, 17], 'teleporter': [106, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Big Jump Room Left', 'id': 213, 'area': 57, 'coordinates': [8, 11], 'teleporter': [108, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Split Corridor Room', 'id': 214, 'area': 57, 'coordinates': [11, 19], 'teleporter': [111, 0]}, {'name': 'Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher', 'id': 215, 'area': 57, 'coordinates': [32, 11], 'teleporter': [107, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - To Plate Corridor', 'id': 216, 'area': 57, 'coordinates': [12, 23], 'teleporter': [109, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - Plate Script', 'id': 217, 'area': 57, 'coordinates': [5, 23], 'teleporter': [47, 8]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Pointless Room', 'id': 218, 'area': 57, 'coordinates': [0, 9], 'teleporter': [110, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room', 'id': 219, 'area': 57, 'coordinates': [0, 15], 'teleporter': [112, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor', 'id': 220, 'area': 57, 'coordinates': [54, 5], 'teleporter': [113, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II', 'id': 221, 'area': 57, 'coordinates': [54, 21], 'teleporter': [114, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1', 'id': 222, 'area': 57, 'coordinates': [62, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2', 'id': 223, 'area': 57, 'coordinates': [63, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3', 'id': 224, 'area': 57, 'coordinates': [62, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4', 'id': 225, 'area': 57, 'coordinates': [63, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor', 'id': 226, 'area': 57, 'coordinates': [50, 25], 'teleporter': [115, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Right Entrance', 'id': 227, 'area': 58, 'coordinates': [55, 28], 'teleporter': [116, 0]}, {'name': 'Lava Dome Jump Maze II - Upper Entrance', 'id': 228, 'area': 58, 'coordinates': [35, 3], 'teleporter': [119, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Left Entrance', 'id': 229, 'area': 58, 'coordinates': [34, 27], 'teleporter': [120, 0]}, {'name': 'Lava Dome Up-Down Corridor - Upper Entrance', 'id': 230, 'area': 58, 'coordinates': [29, 8], 'teleporter': [117, 0]}, {'name': 'Lava Dome Up-Down Corridor - Lower Entrance', 'id': 231, 'area': 58, 'coordinates': [28, 25], 'teleporter': [118, 0]}, {'name': 'Lava Dome Jump Maze I - South Entrance', 'id': 232, 'area': 59, 'coordinates': [20, 27], 'teleporter': [121, 0]}, {'name': 'Lava Dome Jump Maze I - North Entrance', 'id': 233, 'area': 59, 'coordinates': [7, 3], 'teleporter': [122, 0]}, {'name': 'Lava Dome Pointless Room - Entrance', 'id': 234, 'area': 60, 'coordinates': [2, 7], 'teleporter': [123, 0]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 1', 'id': 490, 'area': 60, 'coordinates': [4, 4], 'teleporter': [99, 8]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 2', 'id': 491, 'area': 60, 'coordinates': [4, 5], 'teleporter': [99, 8]}, {'name': 'Lava Dome Lower Moon Helm Room - Left Entrance', 'id': 235, 'area': 60, 'coordinates': [2, 19], 'teleporter': [124, 0]}, {'name': 'Lava Dome Lower Moon Helm Room - Right Entrance', 'id': 236, 'area': 60, 'coordinates': [11, 21], 'teleporter': [125, 0]}, {'name': 'Lava Dome Moon Helm Room - Entrance', 'id': 237, 'area': 60, 'coordinates': [15, 23], 'teleporter': [126, 0]}, {'name': 'Lava Dome Three Jumps Room - To Main Loop', 'id': 238, 'area': 61, 'coordinates': [58, 15], 'teleporter': [127, 0]}, {'name': 'Lava Dome Life Chest Room - Lower South Entrance', 'id': 239, 'area': 61, 'coordinates': [38, 27], 'teleporter': [128, 0]}, {'name': 'Lava Dome Life Chest Room - Upper South Entrance', 'id': 240, 'area': 61, 'coordinates': [28, 23], 'teleporter': [129, 0]}, {'name': 'Lava Dome Big Jump Room - Left Entrance', 'id': 241, 'area': 62, 'coordinates': [42, 51], 'teleporter': [133, 0]}, {'name': 'Lava Dome Big Jump Room - North Entrance', 'id': 242, 'area': 62, 'coordinates': [30, 29], 'teleporter': [131, 0]}, {'name': 'Lava Dome Big Jump Room - Lower Right Stairs', 'id': 243, 'area': 62, 'coordinates': [61, 59], 'teleporter': [132, 0]}, {'name': 'Lava Dome Split Corridor - Upper Stairs', 'id': 244, 'area': 62, 'coordinates': [30, 43], 'teleporter': [130, 0]}, {'name': 'Lava Dome Split Corridor - Lower Stairs', 'id': 245, 'area': 62, 'coordinates': [36, 61], 'teleporter': [134, 0]}, {'name': 'Lava Dome Plate Corridor - Right Entrance', 'id': 246, 'area': 63, 'coordinates': [19, 29], 'teleporter': [135, 0]}, {'name': 'Lava Dome Plate Corridor - Left Entrance', 'id': 247, 'area': 63, 'coordinates': [60, 21], 'teleporter': [137, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Upper Entrance', 'id': 248, 'area': 63, 'coordinates': [22, 3], 'teleporter': [136, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Lower Entrance', 'id': 249, 'area': 63, 'coordinates': [22, 17], 'teleporter': [16, 0]}, {'name': 'Lava Dome Hydra Room - South Entrance', 'id': 250, 'area': 64, 'coordinates': [14, 59], 'teleporter': [105, 3]}, {'name': 'Lava Dome Hydra Room - North Exit', 'id': 251, 'area': 64, 'coordinates': [25, 31], 'teleporter': [138, 0]}, {'name': 'Lava Dome Hydra Room - Hydra Script', 'id': 252, 'area': 64, 'coordinates': [14, 36], 'teleporter': [14, 8]}, {'name': 'Lava Dome Escape Corridor - South Entrance', 'id': 253, 'area': 65, 'coordinates': [22, 17], 'teleporter': [139, 0]}, {'name': 'Lava Dome Escape Corridor - North Entrance', 'id': 254, 'area': 65, 'coordinates': [22, 3], 'teleporter': [9, 0]}, {'name': 'Rope Bridge - West Entrance 1', 'id': 255, 'area': 66, 'coordinates': [3, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 2', 'id': 256, 'area': 66, 'coordinates': [3, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 3', 'id': 257, 'area': 66, 'coordinates': [3, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 4', 'id': 258, 'area': 66, 'coordinates': [3, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 5', 'id': 259, 'area': 66, 'coordinates': [4, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 6', 'id': 260, 'area': 66, 'coordinates': [4, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 7', 'id': 261, 'area': 66, 'coordinates': [4, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 8', 'id': 262, 'area': 66, 'coordinates': [4, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 1', 'id': 263, 'area': 66, 'coordinates': [59, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 2', 'id': 264, 'area': 66, 'coordinates': [59, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 3', 'id': 265, 'area': 66, 'coordinates': [59, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 4', 'id': 266, 'area': 66, 'coordinates': [59, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 5', 'id': 267, 'area': 66, 'coordinates': [60, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 6', 'id': 268, 'area': 66, 'coordinates': [60, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 7', 'id': 269, 'area': 66, 'coordinates': [60, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 8', 'id': 270, 'area': 66, 'coordinates': [60, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - Reuben Fall Script', 'id': 271, 'area': 66, 'coordinates': [13, 12], 'teleporter': [15, 8]}, {'name': 'Alive Forest - West Entrance 1', 'id': 272, 'area': 67, 'coordinates': [8, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - West Entrance 2', 'id': 273, 'area': 67, 'coordinates': [9, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - Giant Tree Entrance', 'id': 274, 'area': 67, 'coordinates': [42, 42], 'teleporter': [143, 0]}, {'name': 'Alive Forest - Libra Teleporter Script', 'id': 275, 'area': 67, 'coordinates': [8, 52], 'teleporter': [64, 8]}, {'name': 'Alive Forest - Gemini Teleporter Script', 'id': 276, 'area': 67, 'coordinates': [57, 49], 'teleporter': [65, 8]}, {'name': 'Alive Forest - Mobius Teleporter Script', 'id': 277, 'area': 67, 'coordinates': [24, 10], 'teleporter': [66, 8]}, {'name': 'Giant Tree 1F - Entrance Script 1', 'id': 278, 'area': 68, 'coordinates': [18, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - Entrance Script 2', 'id': 279, 'area': 68, 'coordinates': [19, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - North Entrance To 2F', 'id': 280, 'area': 68, 'coordinates': [16, 1], 'teleporter': [144, 0]}, {'name': 'Giant Tree 2F Main Lobby - North Entrance to 1F', 'id': 281, 'area': 69, 'coordinates': [44, 33], 'teleporter': [145, 0]}, {'name': 'Giant Tree 2F Main Lobby - Central Entrance to 3F', 'id': 282, 'area': 69, 'coordinates': [42, 47], 'teleporter': [146, 0]}, {'name': 'Giant Tree 2F Main Lobby - West Entrance to Mushroom Room', 'id': 283, 'area': 69, 'coordinates': [58, 49], 'teleporter': [149, 0]}, {'name': 'Giant Tree 2F West Ledge - To 3F Northwest Ledge', 'id': 284, 'area': 69, 'coordinates': [34, 37], 'teleporter': [147, 0]}, {'name': 'Giant Tree 2F Fall From Vine Script', 'id': 482, 'area': 69, 'coordinates': [46, 51], 'teleporter': [76, 8]}, {'name': 'Giant Tree Meteor Chest Room - To 2F Mushroom Room', 'id': 285, 'area': 69, 'coordinates': [58, 44], 'teleporter': [148, 0]}, {'name': 'Giant Tree 2F Mushroom Room - Entrance', 'id': 286, 'area': 70, 'coordinates': [55, 18], 'teleporter': [150, 0]}, {'name': 'Giant Tree 2F Mushroom Room - North Face to Meteor', 'id': 287, 'area': 70, 'coordinates': [56, 7], 'teleporter': [151, 0]}, {'name': 'Giant Tree 3F Central Room - Central Entrance to 2F', 'id': 288, 'area': 71, 'coordinates': [46, 53], 'teleporter': [152, 0]}, {'name': 'Giant Tree 3F Central Room - East Entrance to Worm Room', 'id': 289, 'area': 71, 'coordinates': [58, 39], 'teleporter': [153, 0]}, {'name': 'Giant Tree 3F Lower Corridor - Entrance from Worm Room', 'id': 290, 'area': 71, 'coordinates': [45, 39], 'teleporter': [154, 0]}, {'name': 'Giant Tree 3F West Platform - Lower Entrance', 'id': 291, 'area': 71, 'coordinates': [33, 43], 'teleporter': [155, 0]}, {'name': 'Giant Tree 3F West Platform - Top Entrance', 'id': 292, 'area': 71, 'coordinates': [52, 25], 'teleporter': [156, 0]}, {'name': 'Giant Tree Worm Room - East Entrance', 'id': 293, 'area': 72, 'coordinates': [20, 58], 'teleporter': [157, 0]}, {'name': 'Giant Tree Worm Room - West Entrance', 'id': 294, 'area': 72, 'coordinates': [6, 56], 'teleporter': [158, 0]}, {'name': 'Giant Tree 4F Lower Floor - Entrance', 'id': 295, 'area': 73, 'coordinates': [20, 7], 'teleporter': [159, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower West Mouth', 'id': 296, 'area': 73, 'coordinates': [8, 23], 'teleporter': [160, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower Central Mouth', 'id': 297, 'area': 73, 'coordinates': [14, 25], 'teleporter': [161, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower East Mouth', 'id': 298, 'area': 73, 'coordinates': [20, 25], 'teleporter': [162, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper West Mouth', 'id': 299, 'area': 73, 'coordinates': [8, 19], 'teleporter': [163, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper Central Mouth', 'id': 300, 'area': 73, 'coordinates': [12, 17], 'teleporter': [164, 0]}, {'name': 'Giant Tree 4F Slime Room - Exit', 'id': 301, 'area': 74, 'coordinates': [47, 10], 'teleporter': [165, 0]}, {'name': 'Giant Tree 4F Slime Room - West Entrance', 'id': 302, 'area': 74, 'coordinates': [45, 24], 'teleporter': [166, 0]}, {'name': 'Giant Tree 4F Slime Room - Central Entrance', 'id': 303, 'area': 74, 'coordinates': [50, 24], 'teleporter': [167, 0]}, {'name': 'Giant Tree 4F Slime Room - East Entrance', 'id': 304, 'area': 74, 'coordinates': [57, 28], 'teleporter': [168, 0]}, {'name': 'Giant Tree 5F - Entrance', 'id': 305, 'area': 75, 'coordinates': [14, 51], 'teleporter': [169, 0]}, {'name': 'Giant Tree 5F - Giant Tree Face', 'id': 306, 'area': 75, 'coordinates': [14, 37], 'teleporter': [170, 0]}, {'name': 'Kaidge Temple - Entrance', 'id': 307, 'area': 77, 'coordinates': [44, 63], 'teleporter': [18, 6]}, {'name': 'Kaidge Temple - Mobius Teleporter Script', 'id': 308, 'area': 77, 'coordinates': [35, 57], 'teleporter': [71, 8]}, {'name': 'Windhole Temple - Entrance', 'id': 309, 'area': 78, 'coordinates': [10, 29], 'teleporter': [173, 0]}, {'name': 'Mount Gale - Entrance 1', 'id': 310, 'area': 79, 'coordinates': [1, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Entrance 2', 'id': 311, 'area': 79, 'coordinates': [2, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Visit Quest', 'id': 494, 'area': 79, 'coordinates': [44, 7], 'teleporter': [101, 8]}, {'name': 'Windia - Main Entrance 1', 'id': 312, 'area': 80, 'coordinates': [12, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 2', 'id': 313, 'area': 80, 'coordinates': [13, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 3', 'id': 314, 'area': 80, 'coordinates': [14, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 4', 'id': 315, 'area': 80, 'coordinates': [15, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 5', 'id': 316, 'area': 80, 'coordinates': [12, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 6', 'id': 317, 'area': 80, 'coordinates': [13, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 7', 'id': 318, 'area': 80, 'coordinates': [14, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 8', 'id': 319, 'area': 80, 'coordinates': [15, 41], 'teleporter': [10, 6]}, {'name': "Windia - Otto's House", 'id': 320, 'area': 80, 'coordinates': [21, 39], 'teleporter': [30, 5]}, {'name': "Windia - INN's Script", 'id': 321, 'area': 80, 'coordinates': [18, 34], 'teleporter': [97, 8]}, {'name': 'Windia - Vendor House', 'id': 322, 'area': 80, 'coordinates': [8, 36], 'teleporter': [32, 5]}, {'name': 'Windia - Kid House', 'id': 323, 'area': 80, 'coordinates': [7, 23], 'teleporter': [176, 4]}, {'name': 'Windia - Old People House', 'id': 324, 'area': 80, 'coordinates': [19, 21], 'teleporter': [177, 4]}, {'name': 'Windia - Rainbow Bridge Script', 'id': 325, 'area': 80, 'coordinates': [21, 9], 'teleporter': [10, 6]}, {'name': "Otto's House - Attic Stairs", 'id': 326, 'area': 81, 'coordinates': [2, 19], 'teleporter': [33, 2]}, {'name': "Otto's House - Entrance", 'id': 327, 'area': 81, 'coordinates': [9, 30], 'teleporter': [106, 3]}, {'name': "Otto's Attic - Stairs", 'id': 328, 'area': 81, 'coordinates': [26, 23], 'teleporter': [107, 3]}, {'name': 'Windia Kid House - Entrance Script', 'id': 329, 'area': 82, 'coordinates': [7, 10], 'teleporter': [178, 0]}, {'name': 'Windia Kid House - Basement Stairs', 'id': 330, 'area': 82, 'coordinates': [1, 4], 'teleporter': [180, 0]}, {'name': 'Windia Old People House - Entrance', 'id': 331, 'area': 82, 'coordinates': [55, 12], 'teleporter': [179, 0]}, {'name': 'Windia Old People House - Basement Stairs', 'id': 332, 'area': 82, 'coordinates': [60, 5], 'teleporter': [181, 0]}, {'name': 'Windia Kid House Basement - Stairs', 'id': 333, 'area': 82, 'coordinates': [43, 8], 'teleporter': [182, 0]}, {'name': 'Windia Kid House Basement - Mobius Teleporter', 'id': 334, 'area': 82, 'coordinates': [41, 9], 'teleporter': [44, 8]}, {'name': 'Windia Old People House Basement - Stairs', 'id': 335, 'area': 82, 'coordinates': [39, 26], 'teleporter': [183, 0]}, {'name': 'Windia Old People House Basement - Mobius Teleporter Script', 'id': 336, 'area': 82, 'coordinates': [39, 23], 'teleporter': [43, 8]}, {'name': 'Windia Inn Lobby - Stairs to Beds', 'id': 337, 'area': 82, 'coordinates': [45, 24], 'teleporter': [102, 8]}, {'name': 'Windia Inn Lobby - Exit', 'id': 338, 'area': 82, 'coordinates': [53, 30], 'teleporter': [135, 3]}, {'name': 'Windia Inn Beds - Stairs to Lobby', 'id': 339, 'area': 82, 'coordinates': [33, 59], 'teleporter': [216, 0]}, {'name': 'Windia Vendor House - Entrance', 'id': 340, 'area': 82, 'coordinates': [29, 14], 'teleporter': [108, 3]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 1', 'id': 341, 'area': 83, 'coordinates': [47, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 2', 'id': 342, 'area': 83, 'coordinates': [47, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 3', 'id': 343, 'area': 83, 'coordinates': [48, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 4', 'id': 344, 'area': 83, 'coordinates': [48, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - East Entrance', 'id': 345, 'area': 83, 'coordinates': [55, 12], 'teleporter': [185, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - South Stairs', 'id': 346, 'area': 83, 'coordinates': [51, 25], 'teleporter': [186, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 1', 'id': 347, 'area': 83, 'coordinates': [47, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 2', 'id': 348, 'area': 83, 'coordinates': [48, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Boxes Room - West Stairs', 'id': 349, 'area': 83, 'coordinates': [38, 17], 'teleporter': [187, 0]}, {'name': 'Pazuzu 2F - West Upper Stairs', 'id': 350, 'area': 84, 'coordinates': [7, 11], 'teleporter': [188, 0]}, {'name': 'Pazuzu 2F - South Stairs', 'id': 351, 'area': 84, 'coordinates': [20, 24], 'teleporter': [189, 0]}, {'name': 'Pazuzu 2F - West Lower Stairs', 'id': 352, 'area': 84, 'coordinates': [6, 17], 'teleporter': [190, 0]}, {'name': 'Pazuzu 2F - Central Stairs', 'id': 353, 'area': 84, 'coordinates': [15, 15], 'teleporter': [191, 0]}, {'name': 'Pazuzu 2F - Pazuzu 1', 'id': 354, 'area': 84, 'coordinates': [15, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 2F - Pazuzu 2', 'id': 355, 'area': 84, 'coordinates': [16, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 3F Main Room - North Stairs', 'id': 356, 'area': 85, 'coordinates': [23, 11], 'teleporter': [192, 0]}, {'name': 'Pazuzu 3F Main Room - West Stairs', 'id': 357, 'area': 85, 'coordinates': [7, 15], 'teleporter': [193, 0]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 1', 'id': 358, 'area': 85, 'coordinates': [15, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 2', 'id': 359, 'area': 85, 'coordinates': [16, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Central Island - Central Stairs', 'id': 360, 'area': 85, 'coordinates': [15, 14], 'teleporter': [194, 0]}, {'name': 'Pazuzu 3F Central Island - South Stairs', 'id': 361, 'area': 85, 'coordinates': [17, 25], 'teleporter': [195, 0]}, {'name': 'Pazuzu 4F - Northwest Stairs', 'id': 362, 'area': 86, 'coordinates': [39, 12], 'teleporter': [196, 0]}, {'name': 'Pazuzu 4F - Southwest Stairs', 'id': 363, 'area': 86, 'coordinates': [39, 19], 'teleporter': [197, 0]}, {'name': 'Pazuzu 4F - South Stairs', 'id': 364, 'area': 86, 'coordinates': [47, 24], 'teleporter': [198, 0]}, {'name': 'Pazuzu 4F - Northeast Stairs', 'id': 365, 'area': 86, 'coordinates': [54, 9], 'teleporter': [199, 0]}, {'name': 'Pazuzu 4F - Pazuzu Script 1', 'id': 366, 'area': 86, 'coordinates': [47, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 4F - Pazuzu Script 2', 'id': 367, 'area': 86, 'coordinates': [48, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 5F Pazuzu Loop - West Stairs', 'id': 368, 'area': 87, 'coordinates': [9, 49], 'teleporter': [200, 0]}, {'name': 'Pazuzu 5F Pazuzu Loop - South Stairs', 'id': 369, 'area': 87, 'coordinates': [16, 55], 'teleporter': [201, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northeast Stairs', 'id': 370, 'area': 87, 'coordinates': [22, 40], 'teleporter': [202, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northwest Stairs', 'id': 371, 'area': 87, 'coordinates': [9, 40], 'teleporter': [203, 0]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 1', 'id': 372, 'area': 87, 'coordinates': [15, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 2', 'id': 373, 'area': 87, 'coordinates': [16, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 6F - West Stairs', 'id': 374, 'area': 88, 'coordinates': [41, 47], 'teleporter': [204, 0]}, {'name': 'Pazuzu 6F - Northwest Stairs', 'id': 375, 'area': 88, 'coordinates': [41, 40], 'teleporter': [205, 0]}, {'name': 'Pazuzu 6F - Northeast Stairs', 'id': 376, 'area': 88, 'coordinates': [54, 40], 'teleporter': [206, 0]}, {'name': 'Pazuzu 6F - South Stairs', 'id': 377, 'area': 88, 'coordinates': [52, 56], 'teleporter': [207, 0]}, {'name': 'Pazuzu 6F - Pazuzu Script 1', 'id': 378, 'area': 88, 'coordinates': [47, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 6F - Pazuzu Script 2', 'id': 379, 'area': 88, 'coordinates': [48, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 7F Main Room - Southwest Stairs', 'id': 380, 'area': 89, 'coordinates': [15, 54], 'teleporter': [26, 0]}, {'name': 'Pazuzu 7F Main Room - Northeast Stairs', 'id': 381, 'area': 89, 'coordinates': [21, 40], 'teleporter': [27, 0]}, {'name': 'Pazuzu 7F Main Room - Southeast Stairs', 'id': 382, 'area': 89, 'coordinates': [21, 56], 'teleporter': [28, 0]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 1', 'id': 383, 'area': 89, 'coordinates': [15, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 2', 'id': 384, 'area': 89, 'coordinates': [16, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Crystal Script', 'id': 480, 'area': 89, 'coordinates': [15, 40], 'teleporter': [38, 8]}, {'name': 'Pazuzu 1F to 3F - South Stairs', 'id': 385, 'area': 90, 'coordinates': [43, 60], 'teleporter': [29, 0]}, {'name': 'Pazuzu 1F to 3F - North Stairs', 'id': 386, 'area': 90, 'coordinates': [43, 36], 'teleporter': [30, 0]}, {'name': 'Pazuzu 3F to 5F - South Stairs', 'id': 387, 'area': 91, 'coordinates': [43, 60], 'teleporter': [40, 0]}, {'name': 'Pazuzu 3F to 5F - North Stairs', 'id': 388, 'area': 91, 'coordinates': [43, 36], 'teleporter': [41, 0]}, {'name': 'Pazuzu 5F to 7F - South Stairs', 'id': 389, 'area': 92, 'coordinates': [43, 60], 'teleporter': [38, 0]}, {'name': 'Pazuzu 5F to 7F - North Stairs', 'id': 390, 'area': 92, 'coordinates': [43, 36], 'teleporter': [39, 0]}, {'name': 'Pazuzu 2F to 4F - South Stairs', 'id': 391, 'area': 93, 'coordinates': [43, 60], 'teleporter': [21, 0]}, {'name': 'Pazuzu 2F to 4F - North Stairs', 'id': 392, 'area': 93, 'coordinates': [43, 36], 'teleporter': [22, 0]}, {'name': 'Pazuzu 4F to 6F - South Stairs', 'id': 393, 'area': 94, 'coordinates': [43, 60], 'teleporter': [2, 0]}, {'name': 'Pazuzu 4F to 6F - North Stairs', 'id': 394, 'area': 94, 'coordinates': [43, 36], 'teleporter': [3, 0]}, {'name': 'Light Temple - Entrance', 'id': 395, 'area': 95, 'coordinates': [28, 57], 'teleporter': [19, 6]}, {'name': 'Light Temple - Mobius Teleporter Script', 'id': 396, 'area': 95, 'coordinates': [29, 37], 'teleporter': [70, 8]}, {'name': 'Light Temple - Visit Quest Script 1', 'id': 492, 'area': 95, 'coordinates': [34, 39], 'teleporter': [100, 8]}, {'name': 'Light Temple - Visit Quest Script 2', 'id': 493, 'area': 95, 'coordinates': [35, 39], 'teleporter': [100, 8]}, {'name': 'Ship Dock - Mobius Teleporter Script', 'id': 397, 'area': 96, 'coordinates': [15, 18], 'teleporter': [61, 8]}, {'name': 'Ship Dock - From Overworld', 'id': 398, 'area': 96, 'coordinates': [15, 11], 'teleporter': [73, 0]}, {'name': 'Ship Dock - Entrance', 'id': 399, 'area': 96, 'coordinates': [15, 23], 'teleporter': [17, 6]}, {'name': 'Mac Ship Deck - East Entrance Script', 'id': 400, 'area': 97, 'coordinates': [26, 40], 'teleporter': [37, 8]}, {'name': 'Mac Ship Deck - Central Stairs Script', 'id': 401, 'area': 97, 'coordinates': [16, 47], 'teleporter': [50, 8]}, {'name': 'Mac Ship Deck - West Stairs Script', 'id': 402, 'area': 97, 'coordinates': [8, 34], 'teleporter': [51, 8]}, {'name': 'Mac Ship Deck - East Stairs Script', 'id': 403, 'area': 97, 'coordinates': [24, 36], 'teleporter': [52, 8]}, {'name': 'Mac Ship Deck - North Stairs Script', 'id': 404, 'area': 97, 'coordinates': [12, 9], 'teleporter': [53, 8]}, {'name': 'Mac Ship B1 Outer Ring - South Stairs', 'id': 405, 'area': 98, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring - West Stairs', 'id': 406, 'area': 98, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring - East Stairs', 'id': 407, 'area': 98, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Outer Ring - Northwest Stairs', 'id': 408, 'area': 98, 'coordinates': [10, 23], 'teleporter': [88, 0]}, {'name': 'Mac Ship B1 Square Room - North Stairs', 'id': 409, 'area': 98, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room - South Stairs', 'id': 410, 'area': 98, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room - Stairs', 'id': 411, 'area': 98, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor - South Stairs', 'id': 412, 'area': 98, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor - North Stairs', 'id': 413, 'area': 98, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B2 South Corridor - South Stairs', 'id': 414, 'area': 99, 'coordinates': [48, 51], 'teleporter': [57, 1]}, {'name': 'Mac Ship B2 South Corridor - North Stairs Script', 'id': 415, 'area': 99, 'coordinates': [48, 38], 'teleporter': [55, 8]}, {'name': 'Mac Ship B2 North Corridor - South Stairs Script', 'id': 416, 'area': 99, 'coordinates': [48, 27], 'teleporter': [56, 8]}, {'name': 'Mac Ship B2 North Corridor - North Stairs Script', 'id': 417, 'area': 99, 'coordinates': [48, 12], 'teleporter': [57, 8]}, {'name': 'Mac Ship B2 Outer Ring - Northwest Stairs Script', 'id': 418, 'area': 99, 'coordinates': [55, 11], 'teleporter': [58, 8]}, {'name': 'Mac Ship B1 Outer Ring Cleared - South Stairs', 'id': 419, 'area': 100, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - West Stairs', 'id': 420, 'area': 100, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - East Stairs', 'id': 421, 'area': 100, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - North Stairs', 'id': 422, 'area': 100, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - South Stairs', 'id': 423, 'area': 100, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room Cleared - Main Stairs', 'id': 424, 'area': 100, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - South Stairs', 'id': 425, 'area': 100, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - North Stairs', 'id': 426, 'area': 100, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - Northwest Stairs', 'id': 427, 'area': 100, 'coordinates': [23, 10], 'teleporter': [88, 0]}, {'name': 'Doom Castle Corridor of Destiny - South Entrance', 'id': 428, 'area': 101, 'coordinates': [59, 29], 'teleporter': [84, 0]}, {'name': 'Doom Castle Corridor of Destiny - Ice Floor Entrance', 'id': 429, 'area': 101, 'coordinates': [59, 21], 'teleporter': [35, 2]}, {'name': 'Doom Castle Corridor of Destiny - Lava Floor Entrance', 'id': 430, 'area': 101, 'coordinates': [59, 13], 'teleporter': [209, 0]}, {'name': 'Doom Castle Corridor of Destiny - Sky Floor Entrance', 'id': 431, 'area': 101, 'coordinates': [59, 5], 'teleporter': [211, 0]}, {'name': 'Doom Castle Corridor of Destiny - Hero Room Entrance', 'id': 432, 'area': 101, 'coordinates': [59, 61], 'teleporter': [13, 2]}, {'name': 'Doom Castle Ice Floor - Entrance', 'id': 433, 'area': 102, 'coordinates': [23, 42], 'teleporter': [109, 3]}, {'name': 'Doom Castle Lava Floor - Entrance', 'id': 434, 'area': 103, 'coordinates': [23, 40], 'teleporter': [210, 0]}, {'name': 'Doom Castle Sky Floor - Entrance', 'id': 435, 'area': 104, 'coordinates': [24, 41], 'teleporter': [212, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 1', 'id': 436, 'area': 106, 'coordinates': [15, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 2', 'id': 437, 'area': 106, 'coordinates': [16, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 3', 'id': 438, 'area': 106, 'coordinates': [15, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 4', 'id': 439, 'area': 106, 'coordinates': [16, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Hero Statue Script', 'id': 440, 'area': 106, 'coordinates': [15, 17], 'teleporter': [24, 8]}, {'name': 'Doom Castle Hero Room - Entrance', 'id': 441, 'area': 106, 'coordinates': [15, 24], 'teleporter': [110, 3]}, {'name': 'Doom Castle Dark King Room - Entrance', 'id': 442, 'area': 107, 'coordinates': [14, 26], 'teleporter': [52, 0]}, {'name': 'Doom Castle Dark King Room - Dark King Script', 'id': 443, 'area': 107, 'coordinates': [14, 15], 'teleporter': [25, 8]}, {'name': 'Doom Castle Dark King Room - Unknown', 'id': 444, 'area': 107, 'coordinates': [47, 54], 'teleporter': [77, 0]}, {'name': 'Overworld - Level Forest', 'id': 445, 'area': 0, 'type': 'Overworld', 'teleporter': [46, 8]}, {'name': 'Overworld - Foresta', 'id': 446, 'area': 0, 'type': 'Overworld', 'teleporter': [2, 1]}, {'name': 'Overworld - Sand Temple', 'id': 447, 'area': 0, 'type': 'Overworld', 'teleporter': [3, 1]}, {'name': 'Overworld - Bone Dungeon', 'id': 448, 'area': 0, 'type': 'Overworld', 'teleporter': [4, 1]}, {'name': 'Overworld - Focus Tower Foresta', 'id': 449, 'area': 0, 'type': 'Overworld', 'teleporter': [5, 1]}, {'name': 'Overworld - Focus Tower Aquaria', 'id': 450, 'area': 0, 'type': 'Overworld', 'teleporter': [19, 1]}, {'name': 'Overworld - Libra Temple', 'id': 451, 'area': 0, 'type': 'Overworld', 'teleporter': [7, 1]}, {'name': 'Overworld - Aquaria', 'id': 452, 'area': 0, 'type': 'Overworld', 'teleporter': [8, 8]}, {'name': 'Overworld - Wintry Cave', 'id': 453, 'area': 0, 'type': 'Overworld', 'teleporter': [10, 1]}, {'name': 'Overworld - Life Temple', 'id': 454, 'area': 0, 'type': 'Overworld', 'teleporter': [11, 1]}, {'name': 'Overworld - Falls Basin', 'id': 455, 'area': 0, 'type': 'Overworld', 'teleporter': [12, 1]}, {'name': 'Overworld - Ice Pyramid', 'id': 456, 'area': 0, 'type': 'Overworld', 'teleporter': [13, 1]}, {'name': "Overworld - Spencer's Place", 'id': 457, 'area': 0, 'type': 'Overworld', 'teleporter': [48, 8]}, {'name': 'Overworld - Wintry Temple', 'id': 458, 'area': 0, 'type': 'Overworld', 'teleporter': [16, 1]}, {'name': 'Overworld - Focus Tower Frozen Strip', 'id': 459, 'area': 0, 'type': 'Overworld', 'teleporter': [17, 1]}, {'name': 'Overworld - Focus Tower Fireburg', 'id': 460, 'area': 0, 'type': 'Overworld', 'teleporter': [18, 1]}, {'name': 'Overworld - Fireburg', 'id': 461, 'area': 0, 'type': 'Overworld', 'teleporter': [20, 1]}, {'name': 'Overworld - Mine', 'id': 462, 'area': 0, 'type': 'Overworld', 'teleporter': [21, 1]}, {'name': 'Overworld - Sealed Temple', 'id': 463, 'area': 0, 'type': 'Overworld', 'teleporter': [22, 1]}, {'name': 'Overworld - Volcano', 'id': 464, 'area': 0, 'type': 'Overworld', 'teleporter': [23, 1]}, {'name': 'Overworld - Lava Dome', 'id': 465, 'area': 0, 'type': 'Overworld', 'teleporter': [24, 1]}, {'name': 'Overworld - Focus Tower Windia', 'id': 466, 'area': 0, 'type': 'Overworld', 'teleporter': [6, 1]}, {'name': 'Overworld - Rope Bridge', 'id': 467, 'area': 0, 'type': 'Overworld', 'teleporter': [25, 1]}, {'name': 'Overworld - Alive Forest', 'id': 468, 'area': 0, 'type': 'Overworld', 'teleporter': [26, 1]}, {'name': 'Overworld - Giant Tree', 'id': 469, 'area': 0, 'type': 'Overworld', 'teleporter': [27, 1]}, {'name': 'Overworld - Kaidge Temple', 'id': 470, 'area': 0, 'type': 'Overworld', 'teleporter': [28, 1]}, {'name': 'Overworld - Windia', 'id': 471, 'area': 0, 'type': 'Overworld', 'teleporter': [29, 1]}, {'name': 'Overworld - Windhole Temple', 'id': 472, 'area': 0, 'type': 'Overworld', 'teleporter': [30, 1]}, {'name': 'Overworld - Mount Gale', 'id': 473, 'area': 0, 'type': 'Overworld', 'teleporter': [31, 1]}, {'name': 'Overworld - Pazuzu Tower', 'id': 474, 'area': 0, 'type': 'Overworld', 'teleporter': [32, 1]}, {'name': 'Overworld - Ship Dock', 'id': 475, 'area': 0, 'type': 'Overworld', 'teleporter': [62, 1]}, {'name': 'Overworld - Doom Castle', 'id': 476, 'area': 0, 'type': 'Overworld', 'teleporter': [33, 1]}, {'name': 'Overworld - Light Temple', 'id': 477, 'area': 0, 'type': 'Overworld', 'teleporter': [34, 1]}, {'name': 'Overworld - Mac Ship', 'id': 478, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Overworld - Mac Ship Doom', 'id': 479, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Dummy House - Bed Script', 'id': 480, 'area': 17, 'coordinates': [40, 56], 'teleporter': [1, 8]}, {'name': 'Dummy House - Entrance', 'id': 481, 'area': 17, 'coordinates': [41, 59], 'teleporter': [0, 10]}] \ No newline at end of file diff --git a/worlds/ffmq/data/rooms.yaml b/worlds/ffmq/data/rooms.yaml deleted file mode 100644 index e0c2e8d7f9fc..000000000000 --- a/worlds/ffmq/data/rooms.yaml +++ /dev/null @@ -1,4026 +0,0 @@ -- name: Overworld - id: 0 - type: "Overworld" - game_objects: [] - links: - - target_room: 220 # To Forest Subregion - access: [] -- name: Subregion Foresta - id: 220 - type: "Subregion" - region: "Foresta" - game_objects: - - name: "Foresta South Battlefield" - object_id: 0x01 - location: "ForestaSouthBattlefield" - location_slot: "ForestaSouthBattlefield" - type: "BattlefieldXp" - access: [] - - name: "Foresta West Battlefield" - object_id: 0x02 - location: "ForestaWestBattlefield" - location_slot: "ForestaWestBattlefield" - type: "BattlefieldItem" - access: [] - - name: "Foresta East Battlefield" - object_id: 0x03 - location: "ForestaEastBattlefield" - location_slot: "ForestaEastBattlefield" - type: "BattlefieldGp" - access: [] - links: - - target_room: 15 # Level Forest - location: "LevelForest" - location_slot: "LevelForest" - entrance: 445 - teleporter: [0x2E, 8] - access: [] - - target_room: 16 # Foresta - location: "Foresta" - location_slot: "Foresta" - entrance: 446 - teleporter: [0x02, 1] - access: [] - - target_room: 24 # Sand Temple - location: "SandTemple" - location_slot: "SandTemple" - entrance: 447 - teleporter: [0x03, 1] - access: [] - - target_room: 25 # Bone Dungeon - location: "BoneDungeon" - location_slot: "BoneDungeon" - entrance: 448 - teleporter: [0x04, 1] - access: [] - - target_room: 3 # Focus Tower Foresta - location: "FocusTowerForesta" - location_slot: "FocusTowerForesta" - entrance: 449 - teleporter: [0x05, 1] - access: [] - - target_room: 221 - access: ["SandCoin"] - - target_room: 224 - access: ["RiverCoin"] - - target_room: 226 - access: ["SunCoin"] -- name: Subregion Aquaria - id: 221 - type: "Subregion" - region: "Aquaria" - game_objects: - - name: "South of Libra Temple Battlefield" - object_id: 0x04 - location: "AquariaBattlefield01" - location_slot: "AquariaBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "East of Libra Temple Battlefield" - object_id: 0x05 - location: "AquariaBattlefield02" - location_slot: "AquariaBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "South of Aquaria Battlefield" - object_id: 0x06 - location: "AquariaBattlefield03" - location_slot: "AquariaBattlefield03" - type: "BattlefieldItem" - access: [] - - name: "South of Wintry Cave Battlefield" - object_id: 0x07 - location: "WintryBattlefield01" - location_slot: "WintryBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "West of Wintry Cave Battlefield" - object_id: 0x08 - location: "WintryBattlefield02" - location_slot: "WintryBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "Ice Pyramid Battlefield" - object_id: 0x09 - location: "PyramidBattlefield01" - location_slot: "PyramidBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 10 # Focus Tower Aquaria - location: "FocusTowerAquaria" - location_slot: "FocusTowerAquaria" - entrance: 450 - teleporter: [0x13, 1] - access: [] - - target_room: 39 # Libra Temple - location: "LibraTemple" - location_slot: "LibraTemple" - entrance: 451 - teleporter: [0x07, 1] - access: [] - - target_room: 40 # Aquaria - location: "Aquaria" - location_slot: "Aquaria" - entrance: 452 - teleporter: [0x08, 8] - access: [] - - target_room: 45 # Wintry Cave - location: "WintryCave" - location_slot: "WintryCave" - entrance: 453 - teleporter: [0x0A, 1] - access: [] - - target_room: 52 # Falls Basin - location: "FallsBasin" - location_slot: "FallsBasin" - entrance: 455 - teleporter: [0x0C, 1] - access: [] - - target_room: 54 # Ice Pyramid - location: "IcePyramid" - location_slot: "IcePyramid" - entrance: 456 - teleporter: [0x0D, 1] # Will be switched to a script - access: [] - - target_room: 220 - access: ["SandCoin"] - - target_room: 224 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["SandCoin", "SunCoin"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Life Temple - id: 222 - type: "Subregion" - region: "LifeTemple" - game_objects: [] - links: - - target_room: 51 # Life Temple - location: "LifeTemple" - location_slot: "LifeTemple" - entrance: 454 - teleporter: [0x0B, 1] - access: [] -- name: Subregion Frozen Fields - id: 223 - type: "Subregion" - region: "AquariaFrozenField" - game_objects: - - name: "North of Libra Temple Battlefield" - object_id: 0x0A - location: "LibraBattlefield01" - location_slot: "LibraBattlefield01" - type: "BattlefieldItem" - access: [] - - name: "Aquaria Frozen Field Battlefield" - object_id: 0x0B - location: "LibraBattlefield02" - location_slot: "LibraBattlefield02" - type: "BattlefieldXp" - access: [] - links: - - target_room: 74 # Wintry Temple - location: "WintryTemple" - location_slot: "WintryTemple" - entrance: 458 - teleporter: [0x10, 1] - access: [] - - target_room: 14 # Focus Tower Frozen Strip - location: "FocusTowerFrozen" - location_slot: "FocusTowerFrozen" - entrance: 459 - teleporter: [0x11, 1] - access: [] - - target_room: 221 - access: [] - - target_room: 225 - access: ["SummerAquaria", "DualheadHydra"] -- name: Subregion Fireburg - id: 224 - type: "Subregion" - region: "Fireburg" - game_objects: - - name: "Path to Fireburg Southern Battlefield" - object_id: 0x0C - location: "FireburgBattlefield01" - location_slot: "FireburgBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Path to Fireburg Central Battlefield" - object_id: 0x0D - location: "FireburgBattlefield02" - location_slot: "FireburgBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Path to Fireburg Northern Battlefield" - object_id: 0x0E - location: "FireburgBattlefield03" - location_slot: "FireburgBattlefield03" - type: "BattlefieldXp" - access: [] - - name: "Sealed Temple Battlefield" - object_id: 0x0F - location: "MineBattlefield01" - location_slot: "MineBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Mine Battlefield" - object_id: 0x10 - location: "MineBattlefield02" - location_slot: "MineBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Boulder Battlefield" - object_id: 0x11 - location: "MineBattlefield03" - location_slot: "MineBattlefield03" - type: "BattlefieldXp" - access: [] - links: - - target_room: 13 # Focus Tower Fireburg - location: "FocusTowerFireburg" - location_slot: "FocusTowerFireburg" - entrance: 460 - teleporter: [0x12, 1] - access: [] - - target_room: 76 # Fireburg - location: "Fireburg" - location_slot: "Fireburg" - entrance: 461 - teleporter: [0x14, 1] - access: [] - - target_room: 84 # Mine - location: "Mine" - location_slot: "Mine" - entrance: 462 - teleporter: [0x15, 1] - access: [] - - target_room: 92 # Sealed Temple - location: "SealedTemple" - location_slot: "SealedTemple" - entrance: 463 - teleporter: [0x16, 1] - access: [] - - target_room: 93 # Volcano - location: "Volcano" - location_slot: "Volcano" - entrance: 464 - teleporter: [0x17, 1] # Also this one / 0x0F, 8 - access: [] - - target_room: 100 # Lava Dome - location: "LavaDome" - location_slot: "LavaDome" - entrance: 465 - teleporter: [0x18, 1] - access: [] - - target_room: 220 - access: ["RiverCoin"] - - target_room: 221 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["RiverCoin", "SunCoin"] - - target_room: 225 - access: ["DualheadHydra"] -- name: Subregion Volcano Battlefield - id: 225 - type: "Subregion" - region: "VolcanoBattlefield" - game_objects: - - name: "Volcano Battlefield" - object_id: 0x12 - location: "VolcanoBattlefield01" - location_slot: "VolcanoBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 224 - access: ["DualheadHydra"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Windia - id: 226 - type: "Subregion" - region: "Windia" - game_objects: - - name: "Kaidge Temple Battlefield" - object_id: 0x13 - location: "WindiaBattlefield01" - location_slot: "WindiaBattlefield01" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - - name: "South of Windia Battlefield" - object_id: 0x14 - location: "WindiaBattlefield02" - location_slot: "WindiaBattlefield02" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - links: - - target_room: 9 # Focus Tower Windia - location: "FocusTowerWindia" - location_slot: "FocusTowerWindia" - entrance: 466 - teleporter: [0x06, 1] - access: [] - - target_room: 123 # Rope Bridge - location: "RopeBridge" - location_slot: "RopeBridge" - entrance: 467 - teleporter: [0x19, 1] - access: [] - - target_room: 124 # Alive Forest - location: "AliveForest" - location_slot: "AliveForest" - entrance: 468 - teleporter: [0x1A, 1] - access: [] - - target_room: 125 # Giant Tree - location: "GiantTree" - location_slot: "GiantTree" - entrance: 469 - teleporter: [0x1B, 1] - access: ["Barred"] - - target_room: 152 # Kaidge Temple - location: "KaidgeTemple" - location_slot: "KaidgeTemple" - entrance: 470 - teleporter: [0x1C, 1] - access: [] - - target_room: 156 # Windia - location: "Windia" - location_slot: "Windia" - entrance: 471 - teleporter: [0x1D, 1] - access: [] - - target_room: 154 # Windhole Temple - location: "WindholeTemple" - location_slot: "WindholeTemple" - entrance: 472 - teleporter: [0x1E, 1] - access: [] - - target_room: 155 # Mount Gale - location: "MountGale" - location_slot: "MountGale" - entrance: 473 - teleporter: [0x1F, 1] - access: [] - - target_room: 166 # Pazuzu Tower - location: "PazuzusTower" - location_slot: "PazuzusTower" - entrance: 474 - teleporter: [0x20, 1] - access: [] - - target_room: 220 - access: ["SunCoin"] - - target_room: 221 - access: ["SandCoin", "SunCoin"] - - target_room: 224 - access: ["RiverCoin", "SunCoin"] - - target_room: 227 - access: ["RainbowBridge"] -- name: Subregion Spencer's Cave - id: 227 - type: "Subregion" - region: "SpencerCave" - game_objects: [] - links: - - target_room: 73 # Spencer's Place - location: "SpencersPlace" - location_slot: "SpencersPlace" - entrance: 457 - teleporter: [0x30, 8] - access: [] - - target_room: 226 - access: ["RainbowBridge"] -- name: Subregion Ship Dock - id: 228 - type: "Subregion" - region: "ShipDock" - game_objects: [] - links: - - target_room: 186 # Ship Dock - location: "ShipDock" - location_slot: "ShipDock" - entrance: 475 - teleporter: [0x3E, 1] - access: [] - - target_room: 229 - access: ["ShipLiberated", "ShipDockAccess"] -- name: Subregion Mac's Ship - id: 229 - type: "Subregion" - region: "MacShip" - game_objects: [] - links: - - target_room: 187 # Mac Ship - location: "MacsShip" - location_slot: "MacsShip" - entrance: 478 - teleporter: [0x24, 1] - access: [] - - target_room: 228 - access: ["ShipLiberated", "ShipDockAccess"] - - target_room: 231 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Subregion Light Temple - id: 230 - type: "Subregion" - region: "LightTemple" - game_objects: [] - links: - - target_room: 185 # Light Temple - location: "LightTemple" - location_slot: "LightTemple" - entrance: 477 - teleporter: [0x23, 1] - access: [] -- name: Subregion Doom Castle - id: 231 - type: "Subregion" - region: "DoomCastle" - game_objects: [] - links: - - target_room: 1 # Doom Castle - location: "DoomCastle" - location_slot: "DoomCastle" - entrance: 476 - teleporter: [0x21, 1] - access: [] - - target_room: 187 # Mac Ship Doom - location: "MacsShipDoom" - location_slot: "MacsShipDoom" - entrance: 479 - teleporter: [0x24, 1] - access: ["Barred"] - - target_room: 229 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Doom Castle - Sand Floor - id: 1 - game_objects: - - name: "Doom Castle B2 - Southeast Chest" - object_id: 0x01 - type: "Chest" - access: ["Bomb"] - - name: "Doom Castle B2 - Bone Ledge Box" - object_id: 0x1E - type: "Box" - access: [] - - name: "Doom Castle B2 - Hook Platform Box" - object_id: 0x1F - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 231 - entrance: 1 - teleporter: [1, 6] - access: [] - - target_room: 5 - entrance: 0 - teleporter: [0, 0] - access: ["DragonClaw", "MegaGrenade"] -- name: Doom Castle - Aero Room - id: 2 - game_objects: - - name: "Doom Castle B2 - Sun Door Chest" - object_id: 0x00 - type: "Chest" - access: [] - links: - - target_room: 4 - entrance: 2 - teleporter: [1, 0] - access: [] -- name: Focus Tower B1 - Main Loop - id: 3 - game_objects: [] - links: - - target_room: 220 - entrance: 3 - teleporter: [2, 6] - access: [] - - target_room: 6 - entrance: 4 - teleporter: [4, 0] - access: [] -- name: Focus Tower B1 - Aero Corridor - id: 4 - game_objects: [] - links: - - target_room: 9 - entrance: 5 - teleporter: [5, 0] - access: [] - - target_room: 2 - entrance: 6 - teleporter: [8, 0] - access: [] -- name: Focus Tower B1 - Inner Loop - id: 5 - game_objects: [] - links: - - target_room: 1 - entrance: 8 - teleporter: [7, 0] - access: [] - - target_room: 201 - entrance: 7 - teleporter: [6, 0] - access: [] -- name: Focus Tower 1F Main Lobby - id: 6 - game_objects: - - name: "Focus Tower 1F - Main Lobby Box" - object_id: 0x21 - type: "Box" - access: [] - links: - - target_room: 3 - entrance: 11 - teleporter: [11, 0] - access: [] - - target_room: 7 - access: ["SandCoin"] - - target_room: 8 - access: ["RiverCoin"] - - target_room: 9 - access: ["SunCoin"] -- name: Focus Tower 1F SandCoin Room - id: 7 - game_objects: [] - links: - - target_room: 6 - access: ["SandCoin"] - - target_room: 10 - entrance: 10 - teleporter: [10, 0] - access: [] -- name: Focus Tower 1F RiverCoin Room - id: 8 - game_objects: [] - links: - - target_room: 6 - access: ["RiverCoin"] - - target_room: 11 - entrance: 14 - teleporter: [14, 0] - access: [] -- name: Focus Tower 1F SunCoin Room - id: 9 - game_objects: [] - links: - - target_room: 6 - access: ["SunCoin"] - - target_room: 4 - entrance: 12 - teleporter: [12, 0] - access: [] - - target_room: 226 - entrance: 9 - teleporter: [3, 6] - access: [] -- name: Focus Tower 1F SkyCoin Room - id: 201 - game_objects: [] - links: - - target_room: 195 - entrance: 13 - teleporter: [13, 0] - access: ["SkyCoin", "FlamerusRex", "IceGolem", "DualheadHydra", "Pazuzu"] - - target_room: 5 - entrance: 15 - teleporter: [15, 0] - access: [] -- name: Focus Tower 2F - Sand Coin Passage - id: 10 - game_objects: - - name: "Focus Tower 2F - Sand Door Chest" - object_id: 0x03 - type: "Chest" - access: [] - links: - - target_room: 221 - entrance: 16 - teleporter: [4, 6] - access: [] - - target_room: 7 - entrance: 17 - teleporter: [17, 0] - access: [] -- name: Focus Tower 2F - River Coin Passage - id: 11 - game_objects: [] - links: - - target_room: 8 - entrance: 18 - teleporter: [18, 0] - access: [] - - target_room: 13 - entrance: 19 - teleporter: [20, 0] - access: [] -- name: Focus Tower 2F - Venus Chest Room - id: 12 - game_objects: - - name: "Focus Tower 2F - Back Door Chest" - object_id: 0x02 - type: "Chest" - access: [] - - name: "Focus Tower 2F - Venus Chest" - object_id: 9 - type: "NPC" - access: ["Bomb", "VenusKey"] - links: - - target_room: 14 - entrance: 20 - teleporter: [19, 0] - access: [] -- name: Focus Tower 3F - Lower Floor - id: 13 - game_objects: - - name: "Focus Tower 3F - River Door Box" - object_id: 0x22 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 22 - teleporter: [6, 6] - access: [] - - target_room: 11 - entrance: 23 - teleporter: [24, 0] - access: [] -- name: Focus Tower 3F - Upper Floor - id: 14 - game_objects: [] - links: - - target_room: 223 - entrance: 24 - teleporter: [5, 6] - access: [] - - target_room: 12 - entrance: 25 - teleporter: [23, 0] - access: [] -- name: Level Forest - id: 15 - game_objects: - - name: "Level Forest - Northwest Box" - object_id: 0x28 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Northeast Box" - object_id: 0x29 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Middle Box" - object_id: 0x2A - type: "Box" - access: [] - - name: "Level Forest - Southwest Box" - object_id: 0x2B - type: "Box" - access: ["Axe"] - - name: "Level Forest - Southeast Box" - object_id: 0x2C - type: "Box" - access: ["Axe"] - - name: "Minotaur" - object_id: 0 - type: "Trigger" - on_trigger: ["Minotaur"] - access: ["Kaeli1"] - - name: "Level Forest - Old Man" - object_id: 0 - type: "NPC" - access: [] - - name: "Level Forest - Kaeli" - object_id: 1 - type: "NPC" - access: ["Kaeli1", "Minotaur"] - links: - - target_room: 220 - entrance: 28 - teleporter: [25, 0] - access: [] -- name: Foresta - id: 16 - game_objects: - - name: "Foresta - Outside Box" - object_id: 0x2D - type: "Box" - access: ["Axe"] - links: - - target_room: 220 - entrance: 38 - teleporter: [31, 0] - access: [] - - target_room: 17 - entrance: 44 - teleporter: [0, 5] - access: [] - - target_room: 18 - entrance: 42 - teleporter: [32, 4] - access: [] - - target_room: 19 - entrance: 43 - teleporter: [33, 0] - access: [] - - target_room: 20 - entrance: 45 - teleporter: [1, 5] - access: [] -- name: Kaeli's House - id: 17 - game_objects: - - name: "Foresta - Kaeli's House Box" - object_id: 0x2E - type: "Box" - access: [] - - name: "Kaeli Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli1"] - access: ["TreeWither"] - - name: "Kaeli 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli2"] - access: ["Kaeli1", "Minotaur", "Elixir"] - links: - - target_room: 16 - entrance: 46 - teleporter: [86, 3] - access: [] -- name: Foresta Houses - Old Man's House Main - id: 18 - game_objects: [] - links: - - target_room: 19 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 47 - teleporter: [34, 0] - access: [] -- name: Foresta Houses - Old Man's House Back - id: 19 - game_objects: - - name: "Foresta - Old Man House Chest" - object_id: 0x05 - type: "Chest" - access: [] - - name: "Old Man Barrel" - object_id: 0 - type: "Trigger" - on_trigger: ["BarrelPushed"] - access: [] - links: - - target_room: 18 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 48 - teleporter: [35, 0] - access: [] -- name: Foresta Houses - Rest House - id: 20 - game_objects: - - name: "Foresta - Rest House Box" - object_id: 0x2F - type: "Box" - access: [] - links: - - target_room: 16 - entrance: 50 - teleporter: [87, 3] - access: [] -- name: Libra Treehouse - id: 21 - game_objects: - - name: "Alive Forest - Libra Treehouse Box" - object_id: 0x32 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 51 - teleporter: [67, 8] - access: ["LibraCrest"] -- name: Gemini Treehouse - id: 22 - game_objects: - - name: "Alive Forest - Gemini Treehouse Box" - object_id: 0x33 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 52 - teleporter: [68, 8] - access: ["GeminiCrest"] -- name: Mobius Treehouse - id: 23 - game_objects: - - name: "Alive Forest - Mobius Treehouse West Box" - object_id: 0x30 - type: "Box" - access: [] - - name: "Alive Forest - Mobius Treehouse East Box" - object_id: 0x31 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 53 - teleporter: [69, 8] - access: ["MobiusCrest"] -- name: Sand Temple - id: 24 - game_objects: - - name: "Tristam Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Tristam"] - access: [] - links: - - target_room: 220 - entrance: 54 - teleporter: [36, 0] - access: [] -- name: Bone Dungeon 1F - id: 25 - game_objects: - - name: "Bone Dungeon 1F - Entrance Room West Box" - object_id: 0x35 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room Middle Box" - object_id: 0x36 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room East Box" - object_id: 0x37 - type: "Box" - access: [] - links: - - target_room: 220 - entrance: 55 - teleporter: [37, 0] - access: [] - - target_room: 26 - entrance: 56 - teleporter: [2, 2] - access: [] -- name: Bone Dungeon B1 - Waterway - id: 26 - game_objects: - - name: "Bone Dungeon B1 - Skull Chest" - object_id: 0x06 - type: "Chest" - access: ["Bomb"] - - name: "Bone Dungeon B1 - Tristam" - object_id: 2 - type: "NPC" - access: ["Tristam"] - - name: "Tristam Bone Dungeon Item Given" - object_id: 0 - type: "Trigger" - on_trigger: ["TristamBoneItemGiven"] - access: ["Tristam"] - links: - - target_room: 25 - entrance: 59 - teleporter: [88, 3] - access: [] - - target_room: 28 - entrance: 57 - teleporter: [3, 2] - access: ["Bomb"] -- name: Bone Dungeon B1 - Checker Room - id: 28 - game_objects: - - name: "Bone Dungeon B1 - Checker Room Box" - object_id: 0x38 - type: "Box" - access: ["Bomb"] - links: - - target_room: 26 - entrance: 61 - teleporter: [89, 3] - access: [] - - target_room: 30 - entrance: 60 - teleporter: [4, 2] - access: [] -- name: Bone Dungeon B1 - Hidden Room - id: 29 - game_objects: - - name: "Bone Dungeon B1 - Ribcage Waterway Box" - object_id: 0x39 - type: "Box" - access: [] - links: - - target_room: 31 - entrance: 62 - teleporter: [91, 3] - access: [] -- name: Bone Dungeon B2 - Exploding Skull Room - First Room - id: 30 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Alcove Box" - object_id: 0x3B - type: "Box" - access: [] - - name: "Long Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["LongSpineBombed"] - access: ["Bomb"] - links: - - target_room: 28 - entrance: 65 - teleporter: [90, 3] - access: [] - - target_room: 31 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Second Room - id: 31 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Looped Hallway Box" - object_id: 0x3A - type: "Box" - access: [] - - name: "Short Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["ShortSpineBombed"] - access: ["Bomb"] - links: - - target_room: 29 - entrance: 63 - teleporter: [5, 2] - access: ["LongSpineBombed"] - - target_room: 32 - access: ["ShortSpineBombed"] - - target_room: 30 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Third Room - id: 32 - game_objects: [] - links: - - target_room: 35 - entrance: 64 - teleporter: [6, 2] - access: [] - - target_room: 31 - access: ["ShortSpineBombed"] -- name: Bone Dungeon B2 - Box Room - id: 33 - game_objects: - - name: "Bone Dungeon B2 - Lone Room Box" - object_id: 0x3D - type: "Box" - access: [] - links: - - target_room: 36 - entrance: 66 - teleporter: [93, 3] - access: [] -- name: Bone Dungeon B2 - Quake Room - id: 34 - game_objects: - - name: "Bone Dungeon B2 - Penultimate Room Chest" - object_id: 0x07 - type: "Chest" - access: [] - links: - - target_room: 37 - entrance: 67 - teleporter: [94, 3] - access: [] -- name: Bone Dungeon B2 - Two Skulls Room - First Room - id: 35 - game_objects: - - name: "Bone Dungeon B2 - Two Skulls Room Box" - object_id: 0x3C - type: "Box" - access: [] - - name: "Skull 1" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull1Bombed"] - access: ["Bomb"] - links: - - target_room: 32 - entrance: 71 - teleporter: [92, 3] - access: [] - - target_room: 36 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Second Room - id: 36 - game_objects: - - name: "Skull 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull2Bombed"] - access: ["Bomb"] - links: - - target_room: 33 - entrance: 68 - teleporter: [7, 2] - access: [] - - target_room: 37 - access: ["Skull2Bombed"] - - target_room: 35 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Third Room - id: 37 - game_objects: [] - links: - - target_room: 34 - entrance: 69 - teleporter: [8, 2] - access: [] - - target_room: 38 - entrance: 70 - teleporter: [9, 2] - access: ["Bomb"] - - target_room: 36 - access: ["Skull2Bombed"] -- name: Bone Dungeon B2 - Boss Room - id: 38 - game_objects: - - name: "Bone Dungeon B2 - North Box" - object_id: 0x3E - type: "Box" - access: [] - - name: "Bone Dungeon B2 - South Box" - object_id: 0x3F - type: "Box" - access: [] - - name: "Bone Dungeon B2 - Flamerus Rex Chest" - object_id: 0x08 - type: "Chest" - access: [] - - name: "Bone Dungeon B2 - Tristam's Treasure Chest" - object_id: 0x04 - type: "Chest" - access: [] - - name: "Flamerus Rex" - object_id: 0 - type: "Trigger" - on_trigger: ["FlamerusRex"] - access: [] - links: - - target_room: 37 - entrance: 74 - teleporter: [95, 3] - access: [] -- name: Libra Temple - id: 39 - game_objects: - - name: "Libra Temple - Box" - object_id: 0x40 - type: "Box" - access: [] - - name: "Phoebe Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Phoebe1"] - access: [] - links: - - target_room: 221 - entrance: 75 - teleporter: [13, 6] - access: [] - - target_room: 51 - entrance: 76 - teleporter: [59, 8] - access: ["LibraCrest"] -- name: Aquaria - id: 40 - game_objects: - - name: "Summer Aquaria" - object_id: 0 - type: "Trigger" - on_trigger: ["SummerAquaria"] - access: ["WakeWater"] - links: - - target_room: 221 - entrance: 77 - teleporter: [8, 6] - access: [] - - target_room: 41 - entrance: 81 - teleporter: [10, 5] - access: [] - - target_room: 42 - entrance: 82 - teleporter: [44, 4] - access: [] - - target_room: 44 - entrance: 83 - teleporter: [11, 5] - access: [] - - target_room: 71 - entrance: 89 - teleporter: [42, 0] - access: ["SummerAquaria"] - - target_room: 71 - entrance: 90 - teleporter: [43, 0] - access: ["SummerAquaria"] -- name: Phoebe's House - id: 41 - game_objects: - - name: "Aquaria - Phoebe's House Chest" - object_id: 0x41 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 93 - teleporter: [5, 8] - access: [] -- name: Aquaria Vendor House - id: 42 - game_objects: - - name: "Aquaria - Vendor" - object_id: 4 - type: "NPC" - access: [] - - name: "Aquaria - Vendor House Box" - object_id: 0x42 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 94 - teleporter: [40, 8] - access: [] - - target_room: 43 - entrance: 95 - teleporter: [47, 0] - access: [] -- name: Aquaria Gemini Room - id: 43 - game_objects: [] - links: - - target_room: 42 - entrance: 97 - teleporter: [48, 0] - access: [] - - target_room: 81 - entrance: 96 - teleporter: [72, 8] - access: ["GeminiCrest"] -- name: Aquaria INN - id: 44 - game_objects: [] - links: - - target_room: 40 - entrance: 98 - teleporter: [75, 8] - access: [] -- name: Wintry Cave 1F - East Ledge - id: 45 - game_objects: - - name: "Wintry Cave 1F - North Box" - object_id: 0x43 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Entrance Box" - object_id: 0x46 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Slippery Cliff Box" - object_id: 0x44 - type: "Box" - access: ["Claw"] - - name: "Wintry Cave 1F - Phoebe" - object_id: 5 - type: "NPC" - access: ["Phoebe1"] - links: - - target_room: 221 - entrance: 99 - teleporter: [49, 0] - access: [] - - target_room: 49 - entrance: 100 - teleporter: [14, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 1F - Central Space - id: 46 - game_objects: - - name: "Wintry Cave 1F - Scenic Overlook Box" - object_id: 0x45 - type: "Box" - access: ["Claw"] - links: - - target_room: 45 - access: ["Claw"] - - target_room: 47 - access: ["Claw"] -- name: Wintry Cave 1F - West Ledge - id: 47 - game_objects: [] - links: - - target_room: 48 - entrance: 101 - teleporter: [15, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 2F - id: 48 - game_objects: - - name: "Wintry Cave 2F - West Left Box" - object_id: 0x47 - type: "Box" - access: [] - - name: "Wintry Cave 2F - West Right Box" - object_id: 0x48 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Left Box" - object_id: 0x49 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Right Box" - object_id: 0x4A - type: "Box" - access: [] - links: - - target_room: 47 - entrance: 104 - teleporter: [97, 3] - access: [] - - target_room: 50 - entrance: 103 - teleporter: [50, 0] - access: [] -- name: Wintry Cave 3F Top - id: 49 - game_objects: - - name: "Wintry Cave 3F - West Box" - object_id: 0x4B - type: "Box" - access: [] - - name: "Wintry Cave 3F - East Box" - object_id: 0x4C - type: "Box" - access: [] - links: - - target_room: 45 - entrance: 105 - teleporter: [96, 3] - access: [] -- name: Wintry Cave 3F Bottom - id: 50 - game_objects: - - name: "Wintry Cave 3F - Squidite Chest" - object_id: 0x09 - type: "Chest" - access: ["Phanquid"] - - name: "Phanquid" - object_id: 0 - type: "Trigger" - on_trigger: ["Phanquid"] - access: [] - - name: "Wintry Cave 3F - Before Boss Box" - object_id: 0x4D - type: "Box" - access: [] - links: - - target_room: 48 - entrance: 106 - teleporter: [51, 0] - access: [] -- name: Life Temple - id: 51 - game_objects: - - name: "Life Temple - Box" - object_id: 0x4E - type: "Box" - access: [] - - name: "Life Temple - Mysterious Man" - object_id: 6 - type: "NPC" - access: [] - links: - - target_room: 222 - entrance: 107 - teleporter: [14, 6] - access: [] - - target_room: 39 - entrance: 108 - teleporter: [60, 8] - access: ["LibraCrest"] -- name: Fall Basin - id: 52 - game_objects: - - name: "Falls Basin - Snow Crab Chest" - object_id: 0x0A - type: "Chest" - access: ["FreezerCrab"] - - name: "Freezer Crab" - object_id: 0 - type: "Trigger" - on_trigger: ["FreezerCrab"] - access: [] - - name: "Falls Basin - Box" - object_id: 0x4F - type: "Box" - access: [] - links: - - target_room: 221 - entrance: 111 - teleporter: [53, 0] - access: [] -- name: Ice Pyramid B1 Taunt Room - id: 53 - game_objects: - - name: "Ice Pyramid B1 - Chest" - object_id: 0x0B - type: "Chest" - access: [] - - name: "Ice Pyramid B1 - West Box" - object_id: 0x50 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - North Box" - object_id: 0x51 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - East Box" - object_id: 0x52 - type: "Box" - access: [] - links: - - target_room: 68 - entrance: 113 - teleporter: [55, 0] - access: [] -- name: Ice Pyramid 1F Maze Lobby - id: 54 - game_objects: - - name: "Ice Pyramid 1F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid1FStatue"] - access: ["Sword"] - links: - - target_room: 221 - entrance: 114 - teleporter: [56, 0] - access: [] - - target_room: 55 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 1F Maze - id: 55 - game_objects: - - name: "Ice Pyramid 1F - East Alcove Chest" - object_id: 0x0D - type: "Chest" - access: [] - - name: "Ice Pyramid 1F - Sandwiched Alcove Box" - object_id: 0x53 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Left Box" - object_id: 0x54 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Right Box" - object_id: 0x55 - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 116 - teleporter: [57, 0] - access: [] - - target_room: 57 - entrance: 117 - teleporter: [58, 0] - access: [] - - target_room: 58 - entrance: 118 - teleporter: [59, 0] - access: [] - - target_room: 59 - entrance: 119 - teleporter: [60, 0] - access: [] - - target_room: 60 - entrance: 120 - teleporter: [61, 0] - access: [] - - target_room: 54 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 2F South Tiled Room - id: 56 - game_objects: - - name: "Ice Pyramid 2F - South Side Glass Door Box" - object_id: 0x57 - type: "Box" - access: ["Sword"] - - name: "Ice Pyramid 2F - South Side East Box" - object_id: 0x5B - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 122 - teleporter: [62, 0] - access: [] - - target_room: 61 - entrance: 123 - teleporter: [67, 0] - access: [] -- name: Ice Pyramid 2F West Room - id: 57 - game_objects: - - name: "Ice Pyramid 2F - Northwest Room Box" - object_id: 0x5A - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 124 - teleporter: [63, 0] - access: [] -- name: Ice Pyramid 2F Center Room - id: 58 - game_objects: - - name: "Ice Pyramid 2F - Center Room Box" - object_id: 0x56 - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 125 - teleporter: [64, 0] - access: [] -- name: Ice Pyramid 2F Small North Room - id: 59 - game_objects: - - name: "Ice Pyramid 2F - North Room Glass Door Box" - object_id: 0x58 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 126 - teleporter: [65, 0] - access: [] -- name: Ice Pyramid 2F North Corridor - id: 60 - game_objects: - - name: "Ice Pyramid 2F - North Corridor Glass Door Box" - object_id: 0x59 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 127 - teleporter: [66, 0] - access: [] - - target_room: 62 - entrance: 128 - teleporter: [68, 0] - access: [] -- name: Ice Pyramid 3F Two Boxes Room - id: 61 - game_objects: - - name: "Ice Pyramid 3F - Staircase Dead End Left Box" - object_id: 0x5E - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Staircase Dead End Right Box" - object_id: 0x5F - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 129 - teleporter: [69, 0] - access: [] -- name: Ice Pyramid 3F Main Loop - id: 62 - game_objects: - - name: "Ice Pyramid 3F - Inner Room North Box" - object_id: 0x5C - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Inner Room South Box" - object_id: 0x5D - type: "Box" - access: [] - - name: "Ice Pyramid 3F - East Alcove Box" - object_id: 0x60 - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Leapfrog Box" - object_id: 0x61 - type: "Box" - access: [] - - name: "Ice Pyramid 3F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid3FStatue"] - access: ["Sword"] - links: - - target_room: 60 - entrance: 130 - teleporter: [70, 0] - access: [] - - target_room: 63 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 3F Blocked Room - id: 63 - game_objects: [] - links: - - target_room: 64 - entrance: 131 - teleporter: [71, 0] - access: [] - - target_room: 62 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 4F Main Loop - id: 64 - game_objects: [] - links: - - target_room: 66 - entrance: 133 - teleporter: [73, 0] - access: [] - - target_room: 63 - entrance: 132 - teleporter: [72, 0] - access: [] - - target_room: 65 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 4F Treasure Room - id: 65 - game_objects: - - name: "Ice Pyramid 4F - Chest" - object_id: 0x0C - type: "Chest" - access: [] - - name: "Ice Pyramid 4F - Northwest Box" - object_id: 0x62 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Left Box" - object_id: 0x63 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Right Box" - object_id: 0x64 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Left Box" - object_id: 0x65 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Right Box" - object_id: 0x66 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Left Box" - object_id: 0x67 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Right Box" - object_id: 0x68 - type: "Box" - access: [] - - name: "Ice Pyramid 4F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid4FStatue"] - access: ["Sword"] - links: - - target_room: 64 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 5F Leap of Faith Room - id: 66 - game_objects: - - name: "Ice Pyramid 5F - Glass Door Left Box" - object_id: 0x69 - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - West Ledge Box" - object_id: 0x6A - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Shelf Box" - object_id: 0x6B - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Leapfrog Box" - object_id: 0x6C - type: "Box" - access: [] - - name: "Ice Pyramid 5F - Glass Door Right Box" - object_id: 0x6D - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - North Box" - object_id: 0x6E - type: "Box" - access: [] - links: - - target_room: 64 - entrance: 134 - teleporter: [74, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 53 - access: ["Bomb", "Claw", "Sword"] -- name: Ice Pyramid 5F Stairs to Ice Golem - id: 67 - game_objects: - - name: "Ice Pyramid 5F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid5FStatue"] - access: ["Sword"] - links: - - target_room: 69 - entrance: 137 - teleporter: [76, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 70 - entrance: 136 - teleporter: [75, 0] - access: [] -- name: Ice Pyramid Climbing Wall Room Lower Space - id: 68 - game_objects: [] - links: - - target_room: 53 - entrance: 139 - teleporter: [78, 0] - access: [] - - target_room: 69 - access: ["Claw"] -- name: Ice Pyramid Climbing Wall Room Upper Space - id: 69 - game_objects: [] - links: - - target_room: 67 - entrance: 140 - teleporter: [79, 0] - access: [] - - target_room: 68 - access: ["Claw"] -- name: Ice Pyramid Ice Golem Room - id: 70 - game_objects: - - name: "Ice Pyramid 6F - Ice Golem Chest" - object_id: 0x0E - type: "Chest" - access: ["IceGolem"] - - name: "Ice Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["IceGolem"] - access: [] - links: - - target_room: 67 - entrance: 141 - teleporter: [80, 0] - access: [] - - target_room: 66 - access: [] -- name: Spencer Waterfall - id: 71 - game_objects: [] - links: - - target_room: 72 - entrance: 143 - teleporter: [81, 0] - access: [] - - target_room: 40 - entrance: 145 - teleporter: [82, 0] - access: [] - - target_room: 40 - entrance: 148 - teleporter: [83, 0] - access: [] -- name: Spencer Cave Normal Main - id: 72 - game_objects: - - name: "Spencer's Cave - Box" - object_id: 0x6F - type: "Box" - access: ["Claw"] - - name: "Spencer's Cave - Spencer" - object_id: 8 - type: "NPC" - access: [] - - name: "Spencer's Cave - Locked Chest" - object_id: 13 - type: "NPC" - access: ["VenusKey"] - links: - - target_room: 71 - entrance: 150 - teleporter: [85, 0] - access: [] -- name: Spencer Cave Normal South Ledge - id: 73 - game_objects: - - name: "Collapse Spencer's Cave" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLiberated"] - access: ["MegaGrenade"] - links: - - target_room: 227 - entrance: 151 - teleporter: [7, 6] - access: [] - - target_room: 203 - access: ["MegaGrenade"] -# - target_room: 72 # access to spencer? -# access: ["MegaGrenade"] -- name: Spencer Cave Caved In Main Loop - id: 203 - game_objects: [] - links: - - target_room: 73 - access: [] - - target_room: 207 - entrance: 156 - teleporter: [36, 8] - access: ["MobiusCrest"] - - target_room: 204 - access: ["Claw"] - - target_room: 205 - access: ["Bomb"] -- name: Spencer Cave Caved In Waters - id: 204 - game_objects: - - name: "Bomb Libra Block" - object_id: 0 - type: "Trigger" - on_trigger: ["SpencerCaveLibraBlockBombed"] - access: ["MegaGrenade", "Claw"] - links: - - target_room: 203 - access: ["Claw"] -- name: Spencer Cave Caved In Libra Nook - id: 205 - game_objects: [] - links: - - target_room: 206 - entrance: 153 - teleporter: [33, 8] - access: ["LibraCrest"] -- name: Spencer Cave Caved In Libra Corridor - id: 206 - game_objects: [] - links: - - target_room: 205 - entrance: 154 - teleporter: [34, 8] - access: ["LibraCrest"] - - target_room: 207 - access: ["SpencerCaveLibraBlockBombed"] -- name: Spencer Cave Caved In Mobius Chest - id: 207 - game_objects: - - name: "Spencer's Cave - Mobius Chest" - object_id: 0x0F - type: "Chest" - access: [] - links: - - target_room: 203 - entrance: 155 - teleporter: [35, 8] - access: ["MobiusCrest"] - - target_room: 206 - access: ["Bomb"] -- name: Wintry Temple Outer Room - id: 74 - game_objects: [] - links: - - target_room: 223 - entrance: 157 - teleporter: [15, 6] - access: [] -- name: Wintry Temple Inner Room - id: 75 - game_objects: - - name: "Wintry Temple - West Box" - object_id: 0x70 - type: "Box" - access: [] - - name: "Wintry Temple - North Box" - object_id: 0x71 - type: "Box" - access: [] - links: - - target_room: 92 - entrance: 158 - teleporter: [62, 8] - access: ["GeminiCrest"] -- name: Fireburg Upper Plaza - id: 76 - game_objects: [] - links: - - target_room: 224 - entrance: 159 - teleporter: [9, 6] - access: [] - - target_room: 80 - entrance: 163 - teleporter: [91, 0] - access: [] - - target_room: 77 - entrance: 164 - teleporter: [98, 8] # original value [16, 2] - access: [] - - target_room: 82 - entrance: 165 - teleporter: [96, 8] # original value [17, 2] - access: [] - - target_room: 208 - access: ["Claw"] -- name: Fireburg Lower Plaza - id: 208 - game_objects: - - name: "Fireburg - Hidden Tunnel Box" - object_id: 0x74 - type: "Box" - access: [] - links: - - target_room: 76 - access: ["Claw"] - - target_room: 78 - entrance: 166 - teleporter: [11, 8] - access: ["MultiKey"] -- name: Reuben's House - id: 77 - game_objects: - - name: "Fireburg - Reuben's House Arion" - object_id: 14 - type: "NPC" - access: ["ReubenDadSaved"] - - name: "Reuben Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Reuben1"] - access: [] - - name: "Fireburg - Reuben's House Box" - object_id: 0x75 - type: "Box" - access: [] - links: - - target_room: 76 - entrance: 167 - teleporter: [98, 3] - access: [] -- name: GrenadeMan's House - id: 78 - game_objects: - - name: "Fireburg - Locked House Man" - object_id: 12 - type: "NPC" - access: [] - links: - - target_room: 208 - entrance: 168 - teleporter: [9, 8] - access: ["MultiKey"] - - target_room: 79 - entrance: 169 - teleporter: [93, 0] - access: [] -- name: GrenadeMan's Mobius Room - id: 79 - game_objects: [] - links: - - target_room: 78 - entrance: 170 - teleporter: [94, 0] - access: [] - - target_room: 161 - entrance: 171 - teleporter: [54, 8] - access: ["MobiusCrest"] -- name: Fireburg Vendor House - id: 80 - game_objects: - - name: "Fireburg - Vendor" - object_id: 11 - type: "NPC" - access: [] - links: - - target_room: 76 - entrance: 172 - teleporter: [95, 0] - access: [] - - target_room: 81 - entrance: 173 - teleporter: [96, 0] - access: [] -- name: Fireburg Gemini Room - id: 81 - game_objects: [] - links: - - target_room: 80 - entrance: 174 - teleporter: [97, 0] - access: [] - - target_room: 43 - entrance: 175 - teleporter: [45, 8] - access: ["GeminiCrest"] -- name: Fireburg Hotel Lobby - id: 82 - game_objects: - - name: "Fireburg - Tristam" - object_id: 10 - type: "NPC" - access: ["Tristam", "TristamBoneItemGiven"] - links: - - target_room: 76 - entrance: 177 - teleporter: [99, 3] - access: [] - - target_room: 83 - entrance: 176 - teleporter: [213, 0] - access: [] -- name: Fireburg Hotel Beds - id: 83 - game_objects: [] - links: - - target_room: 82 - entrance: 178 - teleporter: [214, 0] - access: [] -- name: Mine Exterior North West Platforms - id: 84 - game_objects: [] - links: - - target_room: 224 - entrance: 179 - teleporter: [98, 0] - access: [] - - target_room: 88 - entrance: 181 - teleporter: [20, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] - - target_room: 86 - access: ["Claw"] - - target_room: 87 - access: ["Claw"] -- name: Mine Exterior Central Ledge - id: 85 - game_objects: [] - links: - - target_room: 90 - entrance: 183 - teleporter: [22, 2] - access: ["Bomb"] - - target_room: 84 - access: ["Claw"] -- name: Mine Exterior North Ledge - id: 86 - game_objects: [] - links: - - target_room: 89 - entrance: 182 - teleporter: [21, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] -- name: Mine Exterior South East Platforms - id: 87 - game_objects: - - name: "Jinn" - object_id: 0 - type: "Trigger" - on_trigger: ["Jinn"] - access: [] - links: - - target_room: 91 - entrance: 180 - teleporter: [99, 0] - access: ["Jinn"] - - target_room: 86 - access: [] - - target_room: 85 - access: ["Claw"] -- name: Mine Parallel Room - id: 88 - game_objects: - - name: "Mine - Parallel Room West Box" - object_id: 0x77 - type: "Box" - access: ["Claw"] - - name: "Mine - Parallel Room East Box" - object_id: 0x78 - type: "Box" - access: ["Claw"] - links: - - target_room: 84 - entrance: 185 - teleporter: [100, 3] - access: [] -- name: Mine Crescent Room - id: 89 - game_objects: - - name: "Mine - Crescent Room Chest" - object_id: 0x10 - type: "Chest" - access: [] - links: - - target_room: 86 - entrance: 186 - teleporter: [101, 3] - access: [] -- name: Mine Climbing Room - id: 90 - game_objects: - - name: "Mine - Glitchy Collision Cave Box" - object_id: 0x76 - type: "Box" - access: ["Claw"] - links: - - target_room: 85 - entrance: 187 - teleporter: [102, 3] - access: [] -- name: Mine Cliff - id: 91 - game_objects: - - name: "Mine - Cliff Southwest Box" - object_id: 0x79 - type: "Box" - access: [] - - name: "Mine - Cliff Northwest Box" - object_id: 0x7A - type: "Box" - access: [] - - name: "Mine - Cliff Northeast Box" - object_id: 0x7B - type: "Box" - access: [] - - name: "Mine - Cliff Southeast Box" - object_id: 0x7C - type: "Box" - access: [] - - name: "Mine - Reuben" - object_id: 7 - type: "NPC" - access: ["Reuben1"] - - name: "Reuben's dad Saved" - object_id: 0 - type: "Trigger" - on_trigger: ["ReubenDadSaved"] - access: ["MegaGrenade"] - links: - - target_room: 87 - entrance: 188 - teleporter: [100, 0] - access: [] -- name: Sealed Temple - id: 92 - game_objects: - - name: "Sealed Temple - West Box" - object_id: 0x7D - type: "Box" - access: [] - - name: "Sealed Temple - East Box" - object_id: 0x7E - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 190 - teleporter: [16, 6] - access: [] - - target_room: 75 - entrance: 191 - teleporter: [63, 8] - access: ["GeminiCrest"] -- name: Volcano Base - id: 93 - game_objects: - - name: "Volcano - Base Chest" - object_id: 0x11 - type: "Chest" - access: [] - - name: "Volcano - Base West Box" - object_id: 0x7F - type: "Box" - access: [] - - name: "Volcano - Base East Left Box" - object_id: 0x80 - type: "Box" - access: [] - - name: "Volcano - Base East Right Box" - object_id: 0x81 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 192 - teleporter: [103, 0] - access: [] - - target_room: 98 - entrance: 196 - teleporter: [31, 8] - access: [] - - target_room: 96 - entrance: 197 - teleporter: [30, 8] - access: [] -- name: Volcano Top Left - id: 94 - game_objects: - - name: "Volcano - Medusa Chest" - object_id: 0x12 - type: "Chest" - access: ["Medusa"] - - name: "Medusa" - object_id: 0 - type: "Trigger" - on_trigger: ["Medusa"] - access: [] - - name: "Volcano - Behind Medusa Box" - object_id: 0x82 - type: "Box" - access: [] - links: - - target_room: 209 - entrance: 199 - teleporter: [26, 8] - access: [] -- name: Volcano Top Right - id: 95 - game_objects: - - name: "Volcano - Top of the Volcano Left Box" - object_id: 0x83 - type: "Box" - access: [] - - name: "Volcano - Top of the Volcano Right Box" - object_id: 0x84 - type: "Box" - access: [] - links: - - target_room: 99 - entrance: 200 - teleporter: [79, 8] - access: [] -- name: Volcano Right Path - id: 96 - game_objects: - - name: "Volcano - Right Path Box" - object_id: 0x87 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 201 - teleporter: [15, 8] - access: [] -- name: Volcano Left Path - id: 98 - game_objects: - - name: "Volcano - Left Path Box" - object_id: 0x86 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 204 - teleporter: [27, 8] - access: [] - - target_room: 99 - entrance: 202 - teleporter: [25, 2] - access: [] - - target_room: 209 - entrance: 203 - teleporter: [26, 2] - access: [] -- name: Volcano Cross Left-Right - id: 99 - game_objects: [] - links: - - target_room: 95 - entrance: 206 - teleporter: [29, 8] - access: [] - - target_room: 98 - entrance: 205 - teleporter: [103, 3] - access: [] -- name: Volcano Cross Right-Left - id: 209 - game_objects: - - name: "Volcano - Crossover Section Box" - object_id: 0x85 - type: "Box" - access: [] - links: - - target_room: 98 - entrance: 208 - teleporter: [104, 3] - access: [] - - target_room: 94 - entrance: 207 - teleporter: [28, 8] - access: [] -- name: Lava Dome Inner Ring Main Loop - id: 100 - game_objects: - - name: "Lava Dome - Exterior Caldera Near Switch Cliff Box" - object_id: 0x88 - type: "Box" - access: [] - - name: "Lava Dome - Exterior South Cliff Box" - object_id: 0x89 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 209 - teleporter: [104, 0] - access: [] - - target_room: 113 - entrance: 211 - teleporter: [105, 0] - access: [] - - target_room: 114 - entrance: 212 - teleporter: [106, 0] - access: [] - - target_room: 116 - entrance: 213 - teleporter: [108, 0] - access: [] - - target_room: 118 - entrance: 214 - teleporter: [111, 0] - access: [] -- name: Lava Dome Inner Ring Center Ledge - id: 101 - game_objects: - - name: "Lava Dome - Exterior Center Dropoff Ledge Box" - object_id: 0x8A - type: "Box" - access: [] - links: - - target_room: 115 - entrance: 215 - teleporter: [107, 0] - access: [] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Plate Ledge - id: 102 - game_objects: - - name: "Lava Dome Plate" - object_id: 0 - type: "Trigger" - on_trigger: ["LavaDomePlate"] - access: [] - links: - - target_room: 119 - entrance: 216 - teleporter: [109, 0] - access: [] -- name: Lava Dome Inner Ring Upper Ledge West - id: 103 - game_objects: [] - links: - - target_room: 111 - entrance: 219 - teleporter: [112, 0] - access: [] - - target_room: 108 - entrance: 220 - teleporter: [113, 0] - access: [] - - target_room: 104 - access: ["Claw"] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Upper Ledge East - id: 104 - game_objects: [] - links: - - target_room: 110 - entrance: 218 - teleporter: [110, 0] - access: [] - - target_room: 103 - access: ["Claw"] -- name: Lava Dome Inner Ring Big Door Ledge - id: 105 - game_objects: [] - links: - - target_room: 107 - entrance: 221 - teleporter: [114, 0] - access: [] - - target_room: 121 - entrance: 222 - teleporter: [29, 2] - access: ["LavaDomePlate"] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - id: 106 - game_objects: - - name: "Lava Dome - Exterior Dead End Caldera Box" - object_id: 0x8B - type: "Box" - access: [] - links: - - target_room: 120 - entrance: 226 - teleporter: [115, 0] - access: [] -- name: Lava Dome Jump Maze II - id: 107 - game_objects: - - name: "Lava Dome - Gold Maze Northwest Box" - object_id: 0x8C - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southwest Box" - object_id: 0xF6 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Northeast Box" - object_id: 0xF7 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze North Box" - object_id: 0xF8 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Center Box" - object_id: 0xF9 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southeast Box" - object_id: 0xFA - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 227 - teleporter: [116, 0] - access: [] - - target_room: 108 - entrance: 228 - teleporter: [119, 0] - access: [] - - target_room: 120 - entrance: 229 - teleporter: [120, 0] - access: [] -- name: Lava Dome Up-Down Corridor - id: 108 - game_objects: [] - links: - - target_room: 107 - entrance: 231 - teleporter: [118, 0] - access: [] - - target_room: 103 - entrance: 230 - teleporter: [117, 0] - access: [] -- name: Lava Dome Jump Maze I - id: 109 - game_objects: - - name: "Lava Dome - Bare Maze Leapfrog Alcove North Box" - object_id: 0x8D - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Leapfrog Alcove South Box" - object_id: 0x8E - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Center Box" - object_id: 0x8F - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Southwest Box" - object_id: 0x90 - type: "Box" - access: [] - links: - - target_room: 118 - entrance: 232 - teleporter: [121, 0] - access: [] - - target_room: 111 - entrance: 233 - teleporter: [122, 0] - access: [] -- name: Lava Dome Pointless Room - id: 110 - game_objects: [] - links: - - target_room: 104 - entrance: 234 - teleporter: [123, 0] - access: [] -- name: Lava Dome Lower Moon Helm Room - id: 111 - game_objects: - - name: "Lava Dome - U-Bend Room North Box" - object_id: 0x92 - type: "Box" - access: [] - - name: "Lava Dome - U-Bend Room South Box" - object_id: 0x93 - type: "Box" - access: [] - links: - - target_room: 103 - entrance: 235 - teleporter: [124, 0] - access: [] - - target_room: 109 - entrance: 236 - teleporter: [125, 0] - access: [] -- name: Lava Dome Moon Helm Room - id: 112 - game_objects: - - name: "Lava Dome - Beyond River Room Chest" - object_id: 0x13 - type: "Chest" - access: [] - - name: "Lava Dome - Beyond River Room Box" - object_id: 0x91 - type: "Box" - access: [] - links: - - target_room: 117 - entrance: 237 - teleporter: [126, 0] - access: [] -- name: Lava Dome Three Jumps Room - id: 113 - game_objects: - - name: "Lava Dome - Three Jumps Room Box" - object_id: 0x96 - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 238 - teleporter: [127, 0] - access: [] -- name: Lava Dome Life Chest Room Lower Ledge - id: 114 - game_objects: - - name: "Lava Dome - Gold Bar Room Boulder Chest" - object_id: 0x1C - type: "Chest" - access: ["MegaGrenade"] - links: - - target_room: 100 - entrance: 239 - teleporter: [128, 0] - access: [] - - target_room: 115 - access: ["Claw"] -- name: Lava Dome Life Chest Room Upper Ledge - id: 115 - game_objects: - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box West" - object_id: 0x94 - type: "Box" - access: [] - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box East" - object_id: 0x95 - type: "Box" - access: [] - links: - - target_room: 101 - entrance: 240 - teleporter: [129, 0] - access: [] - - target_room: 114 - access: ["Claw"] -- name: Lava Dome Big Jump Room Main Area - id: 116 - game_objects: - - name: "Lava Dome - Lava River Room North Box" - object_id: 0x98 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room East Box" - object_id: 0x99 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room South Box" - object_id: 0x9A - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 241 - teleporter: [133, 0] - access: [] - - target_room: 119 - entrance: 243 - teleporter: [132, 0] - access: [] - - target_room: 117 - access: ["MegaGrenade"] -- name: Lava Dome Big Jump Room MegaGrenade Area - id: 117 - game_objects: [] - links: - - target_room: 112 - entrance: 242 - teleporter: [131, 0] - access: [] - - target_room: 116 - access: ["Bomb"] -- name: Lava Dome Split Corridor - id: 118 - game_objects: - - name: "Lava Dome - Split Corridor Box" - object_id: 0x97 - type: "Box" - access: [] - links: - - target_room: 109 - entrance: 244 - teleporter: [130, 0] - access: [] - - target_room: 100 - entrance: 245 - teleporter: [134, 0] - access: [] -- name: Lava Dome Plate Corridor - id: 119 - game_objects: [] - links: - - target_room: 102 - entrance: 246 - teleporter: [135, 0] - access: [] - - target_room: 116 - entrance: 247 - teleporter: [137, 0] - access: [] -- name: Lava Dome Four Boxes Stairs - id: 120 - game_objects: - - name: "Lava Dome - Caldera Stairway West Left Box" - object_id: 0x9B - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway West Right Box" - object_id: 0x9C - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Left Box" - object_id: 0x9D - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Right Box" - object_id: 0x9E - type: "Box" - access: [] - links: - - target_room: 107 - entrance: 248 - teleporter: [136, 0] - access: [] - - target_room: 106 - entrance: 249 - teleporter: [16, 0] - access: [] -- name: Lava Dome Hydra Room - id: 121 - game_objects: - - name: "Lava Dome - Dualhead Hydra Chest" - object_id: 0x14 - type: "Chest" - access: ["DualheadHydra"] - - name: "Dualhead Hydra" - object_id: 0 - type: "Trigger" - on_trigger: ["DualheadHydra"] - access: [] - - name: "Lava Dome - Hydra Room Northwest Box" - object_id: 0x9F - type: "Box" - access: [] - - name: "Lava Dome - Hydra Room Southweast Box" - object_id: 0xA0 - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 250 - teleporter: [105, 3] - access: [] - - target_room: 122 - entrance: 251 - teleporter: [138, 0] - access: ["DualheadHydra"] -- name: Lava Dome Escape Corridor - id: 122 - game_objects: [] - links: - - target_room: 121 - entrance: 253 - teleporter: [139, 0] - access: [] -- name: Rope Bridge - id: 123 - game_objects: - - name: "Rope Bridge - West Box" - object_id: 0xA3 - type: "Box" - access: [] - - name: "Rope Bridge - East Box" - object_id: 0xA4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 255 - teleporter: [140, 0] - access: [] -- name: Alive Forest - id: 124 - game_objects: - - name: "Alive Forest - Tree Stump Chest" - object_id: 0x15 - type: "Chest" - access: ["Axe"] - - name: "Alive Forest - Near Entrance Box" - object_id: 0xA5 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - After Bridge Box" - object_id: 0xA6 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - Gemini Stump Box" - object_id: 0xA7 - type: "Box" - access: ["Axe"] - links: - - target_room: 226 - entrance: 272 - teleporter: [142, 0] - access: ["Axe"] - - target_room: 21 - entrance: 275 - teleporter: [64, 8] - access: ["LibraCrest", "Axe"] - - target_room: 22 - entrance: 276 - teleporter: [65, 8] - access: ["GeminiCrest", "Axe"] - - target_room: 23 - entrance: 277 - teleporter: [66, 8] - access: ["MobiusCrest", "Axe"] - - target_room: 125 - entrance: 274 - teleporter: [143, 0] - access: ["Axe"] -- name: Giant Tree 1F Main Area - id: 125 - game_objects: - - name: "Giant Tree 1F - Northwest Box" - object_id: 0xA8 - type: "Box" - access: [] - - name: "Giant Tree 1F - Southwest Box" - object_id: 0xA9 - type: "Box" - access: [] - - name: "Giant Tree 1F - Center Box" - object_id: 0xAA - type: "Box" - access: [] - - name: "Giant Tree 1F - East Box" - object_id: 0xAB - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 278 - teleporter: [56, 1] # [49, 8] script restored if no map shuffling - access: [] - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 1F North Island - id: 202 - game_objects: [] - links: - - target_room: 127 - entrance: 280 - teleporter: [144, 0] - access: [] - - target_room: 125 - access: ["DragonClaw"] -- name: Giant Tree 1F Central Island - id: 126 - game_objects: [] - links: - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 2F Main Lobby - id: 127 - game_objects: - - name: "Giant Tree 2F - North Box" - object_id: 0xAC - type: "Box" - access: [] - links: - - target_room: 126 - access: ["DragonClaw"] - - target_room: 125 - entrance: 281 - teleporter: [145, 0] - access: [] - - target_room: 133 - entrance: 283 - teleporter: [149, 0] - access: [] - - target_room: 129 - access: ["DragonClaw"] -- name: Giant Tree 2F West Ledge - id: 128 - game_objects: - - name: "Giant Tree 2F - Dropdown Ledge Box" - object_id: 0xAE - type: "Box" - access: [] - links: - - target_room: 140 - entrance: 284 - teleporter: [147, 0] - access: ["Sword"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Lower Area - id: 129 - game_objects: - - name: "Giant Tree 2F - South Box" - object_id: 0xAD - type: "Box" - access: [] - links: - - target_room: 130 - access: ["Claw"] - - target_room: 131 - access: ["Claw"] -- name: Giant Tree 2F Central Island - id: 130 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 135 - entrance: 282 - teleporter: [146, 0] - access: ["Sword"] -- name: Giant Tree 2F East Ledge - id: 131 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Meteor Chest Room - id: 132 - game_objects: - - name: "Giant Tree 2F - Gidrah Chest" - object_id: 0x16 - type: "Chest" - access: [] - links: - - target_room: 133 - entrance: 285 - teleporter: [148, 0] - access: [] -- name: Giant Tree 2F Mushroom Room - id: 133 - game_objects: - - name: "Giant Tree 2F - Mushroom Tunnel West Box" - object_id: 0xAF - type: "Box" - access: ["Axe"] - - name: "Giant Tree 2F - Mushroom Tunnel East Box" - object_id: 0xB0 - type: "Box" - access: ["Axe"] - links: - - target_room: 127 - entrance: 286 - teleporter: [150, 0] - access: ["Axe"] - - target_room: 132 - entrance: 287 - teleporter: [151, 0] - access: ["Axe", "Gidrah"] -- name: Giant Tree 3F Central Island - id: 135 - game_objects: - - name: "Giant Tree 3F - Central Island Box" - object_id: 0xB3 - type: "Box" - access: [] - links: - - target_room: 130 - entrance: 288 - teleporter: [152, 0] - access: [] - - target_room: 136 - access: ["Claw"] - - target_room: 137 - access: ["DragonClaw"] -- name: Giant Tree 3F Central Area - id: 136 - game_objects: - - name: "Giant Tree 3F - Center North Box" - object_id: 0xB1 - type: "Box" - access: [] - - name: "Giant Tree 3F - Center West Box" - object_id: 0xB2 - type: "Box" - access: [] - links: - - target_room: 135 - access: ["Claw"] - - target_room: 127 - access: [] - - target_room: 131 - access: [] -- name: Giant Tree 3F Lower Ledge - id: 137 - game_objects: [] - links: - - target_room: 135 - access: ["DragonClaw"] - - target_room: 142 - entrance: 289 - teleporter: [153, 0] - access: ["Sword"] -- name: Giant Tree 3F West Area - id: 138 - game_objects: - - name: "Giant Tree 3F - West Side Box" - object_id: 0xB4 - type: "Box" - access: [] - links: - - target_room: 128 - access: [] - - target_room: 210 - entrance: 290 - teleporter: [154, 0] - access: [] -- name: Giant Tree 3F Middle Up Island - id: 139 - game_objects: [] - links: - - target_room: 136 - access: ["Claw"] -- name: Giant Tree 3F West Platform - id: 140 - game_objects: [] - links: - - target_room: 139 - access: ["Claw"] - - target_room: 141 - access: ["Claw"] - - target_room: 128 - entrance: 291 - teleporter: [155, 0] - access: [] -- name: Giant Tree 3F North Ledge - id: 141 - game_objects: [] - links: - - target_room: 143 - entrance: 292 - teleporter: [156, 0] - access: ["Sword"] - - target_room: 139 - access: ["Claw"] - - target_room: 136 - access: ["Claw"] -- name: Giant Tree Worm Room Upper Ledge - id: 142 - game_objects: - - name: "Giant Tree 3F - Worm Room North Box" - object_id: 0xB5 - type: "Box" - access: ["Axe"] - - name: "Giant Tree 3F - Worm Room South Box" - object_id: 0xB6 - type: "Box" - access: ["Axe"] - links: - - target_room: 137 - entrance: 293 - teleporter: [157, 0] - access: ["Axe"] - - target_room: 210 - access: ["Axe", "Claw"] -- name: Giant Tree Worm Room Lower Ledge - id: 210 - game_objects: [] - links: - - target_room: 138 - entrance: 294 - teleporter: [158, 0] - access: [] -- name: Giant Tree 4F Lower Floor - id: 143 - game_objects: [] - links: - - target_room: 141 - entrance: 295 - teleporter: [159, 0] - access: [] - - target_room: 148 - entrance: 296 - teleporter: [160, 0] - access: [] - - target_room: 148 - entrance: 297 - teleporter: [161, 0] - access: [] - - target_room: 147 - entrance: 298 - teleporter: [162, 0] - access: ["Sword"] -- name: Giant Tree 4F Middle Floor - id: 144 - game_objects: - - name: "Giant Tree 4F - Highest Platform North Box" - object_id: 0xB7 - type: "Box" - access: [] - - name: "Giant Tree 4F - Highest Platform South Box" - object_id: 0xB8 - type: "Box" - access: [] - links: - - target_room: 149 - entrance: 299 - teleporter: [163, 0] - access: [] - - target_room: 145 - access: ["Claw"] - - target_room: 146 - access: ["DragonClaw"] -- name: Giant Tree 4F Upper Floor - id: 145 - game_objects: [] - links: - - target_room: 150 - entrance: 300 - teleporter: [164, 0] - access: ["Sword"] - - target_room: 144 - access: ["Claw"] -- name: Giant Tree 4F South Ledge - id: 146 - game_objects: - - name: "Giant Tree 4F - Hook Ledge Northeast Box" - object_id: 0xB9 - type: "Box" - access: [] - - name: "Giant Tree 4F - Hook Ledge Southwest Box" - object_id: 0xBA - type: "Box" - access: [] - links: - - target_room: 144 - access: ["DragonClaw"] -- name: Giant Tree 4F Slime Room East Area - id: 147 - game_objects: - - name: "Giant Tree 4F - East Slime Room Box" - object_id: 0xBC - type: "Box" - access: ["Axe"] - links: - - target_room: 143 - entrance: 304 - teleporter: [168, 0] - access: [] -- name: Giant Tree 4F Slime Room West Area - id: 148 - game_objects: [] - links: - - target_room: 143 - entrance: 303 - teleporter: [167, 0] - access: ["Axe"] - - target_room: 143 - entrance: 302 - teleporter: [166, 0] - access: ["Axe"] - - target_room: 149 - access: ["Axe", "Claw"] -- name: Giant Tree 4F Slime Room Platform - id: 149 - game_objects: - - name: "Giant Tree 4F - West Slime Room Box" - object_id: 0xBB - type: "Box" - access: [] - links: - - target_room: 144 - entrance: 301 - teleporter: [165, 0] - access: [] - - target_room: 148 - access: ["Claw"] -- name: Giant Tree 5F Lower Area - id: 150 - game_objects: - - name: "Giant Tree 5F - Northwest Left Box" - object_id: 0xBD - type: "Box" - access: [] - - name: "Giant Tree 5F - Northwest Right Box" - object_id: 0xBE - type: "Box" - access: [] - - name: "Giant Tree 5F - South Left Box" - object_id: 0xBF - type: "Box" - access: [] - - name: "Giant Tree 5F - South Right Box" - object_id: 0xC0 - type: "Box" - access: [] - links: - - target_room: 145 - entrance: 305 - teleporter: [169, 0] - access: [] - - target_room: 151 - access: ["Claw"] - - target_room: 143 - access: [] -- name: Giant Tree 5F Gidrah Platform - id: 151 - game_objects: - - name: "Gidrah" - object_id: 0 - type: "Trigger" - on_trigger: ["Gidrah"] - access: [] - links: - - target_room: 150 - access: ["Claw"] -- name: Kaidge Temple Lower Ledge - id: 152 - game_objects: [] - links: - - target_room: 226 - entrance: 307 - teleporter: [18, 6] - access: [] - - target_room: 153 - access: ["Claw"] -- name: Kaidge Temple Upper Ledge - id: 153 - game_objects: - - name: "Kaidge Temple - Box" - object_id: 0xC1 - type: "Box" - access: [] - links: - - target_room: 185 - entrance: 308 - teleporter: [71, 8] - access: ["MobiusCrest"] - - target_room: 152 - access: ["Claw"] -- name: Windhole Temple - id: 154 - game_objects: - - name: "Windhole Temple - Box" - object_id: 0xC2 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 309 - teleporter: [173, 0] - access: [] -- name: Mount Gale - id: 155 - game_objects: - - name: "Mount Gale - Dullahan Chest" - object_id: 0x17 - type: "Chest" - access: ["DragonClaw", "Dullahan"] - - name: "Dullahan" - object_id: 0 - type: "Trigger" - on_trigger: ["Dullahan"] - access: ["DragonClaw"] - - name: "Mount Gale - East Box" - object_id: 0xC3 - type: "Box" - access: ["DragonClaw"] - - name: "Mount Gale - West Box" - object_id: 0xC4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 310 - teleporter: [174, 0] - access: [] -- name: Windia - id: 156 - game_objects: [] - links: - - target_room: 226 - entrance: 312 - teleporter: [10, 6] - access: [] - - target_room: 157 - entrance: 320 - teleporter: [30, 5] - access: [] - - target_room: 163 - entrance: 321 - teleporter: [97, 8] - access: [] - - target_room: 165 - entrance: 322 - teleporter: [32, 5] - access: [] - - target_room: 159 - entrance: 323 - teleporter: [176, 4] - access: [] - - target_room: 160 - entrance: 324 - teleporter: [177, 4] - access: [] -- name: Otto's House - id: 157 - game_objects: - - name: "Otto" - object_id: 0 - type: "Trigger" - on_trigger: ["RainbowBridge"] - access: ["ThunderRock"] - links: - - target_room: 156 - entrance: 327 - teleporter: [106, 3] - access: [] - - target_room: 158 - entrance: 326 - teleporter: [33, 2] - access: [] -- name: Otto's Attic - id: 158 - game_objects: - - name: "Windia - Otto's Attic Box" - object_id: 0xC5 - type: "Box" - access: [] - links: - - target_room: 157 - entrance: 328 - teleporter: [107, 3] - access: [] -- name: Windia Kid House - id: 159 - game_objects: [] - links: - - target_room: 156 - entrance: 329 - teleporter: [178, 0] - access: [] - - target_room: 161 - entrance: 330 - teleporter: [180, 0] - access: [] -- name: Windia Old People House - id: 160 - game_objects: [] - links: - - target_room: 156 - entrance: 331 - teleporter: [179, 0] - access: [] - - target_room: 162 - entrance: 332 - teleporter: [181, 0] - access: [] -- name: Windia Kid House Basement - id: 161 - game_objects: [] - links: - - target_room: 159 - entrance: 333 - teleporter: [182, 0] - access: [] - - target_room: 79 - entrance: 334 - teleporter: [44, 8] - access: ["MobiusCrest"] -- name: Windia Old People House Basement - id: 162 - game_objects: - - name: "Windia - Mobius Basement West Box" - object_id: 0xC8 - type: "Box" - access: [] - - name: "Windia - Mobius Basement East Box" - object_id: 0xC9 - type: "Box" - access: [] - links: - - target_room: 160 - entrance: 335 - teleporter: [183, 0] - access: [] - - target_room: 186 - entrance: 336 - teleporter: [43, 8] - access: ["MobiusCrest"] -- name: Windia Inn Lobby - id: 163 - game_objects: [] - links: - - target_room: 156 - entrance: 338 - teleporter: [135, 3] - access: [] - - target_room: 164 - entrance: 337 - teleporter: [102, 8] - access: [] -- name: Windia Inn Beds - id: 164 - game_objects: - - name: "Windia - Inn Bedroom North Box" - object_id: 0xC6 - type: "Box" - access: [] - - name: "Windia - Inn Bedroom South Box" - object_id: 0xC7 - type: "Box" - access: [] - - name: "Windia - Kaeli" - object_id: 15 - type: "NPC" - access: ["Kaeli2"] - links: - - target_room: 163 - entrance: 339 - teleporter: [216, 0] - access: [] -- name: Windia Vendor House - id: 165 - game_objects: - - name: "Windia - Vendor" - object_id: 16 - type: "NPC" - access: [] - links: - - target_room: 156 - entrance: 340 - teleporter: [108, 3] - access: [] -- name: Pazuzu Tower 1F Main Lobby - id: 166 - game_objects: - - name: "Pazuzu 1F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu1F"] - access: [] - links: - - target_room: 226 - entrance: 341 - teleporter: [184, 0] - access: [] - - target_room: 180 - entrance: 345 - teleporter: [185, 0] - access: [] -- name: Pazuzu Tower 1F Boxes Room - id: 167 - game_objects: - - name: "Pazuzu's Tower 1F - Descent Bomb Wall West Box" - object_id: 0xCA - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall Center Box" - object_id: 0xCB - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall East Box" - object_id: 0xCC - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Box" - object_id: 0xCD - type: "Box" - access: [] - links: - - target_room: 169 - entrance: 349 - teleporter: [187, 0] - access: [] -- name: Pazuzu Tower 1F Southern Platform - id: 168 - game_objects: [] - links: - - target_room: 169 - entrance: 346 - teleporter: [186, 0] - access: [] - - target_room: 166 - access: ["DragonClaw"] -- name: Pazuzu 2F - id: 169 - game_objects: - - name: "Pazuzu's Tower 2F - East Room West Box" - object_id: 0xCE - type: "Box" - access: [] - - name: "Pazuzu's Tower 2F - East Room East Box" - object_id: 0xCF - type: "Box" - access: [] - - name: "Pazuzu 2F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2FLock"] - access: ["Axe"] - - name: "Pazuzu 2F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 350 - teleporter: [188, 0] - access: [] - - target_room: 168 - entrance: 351 - teleporter: [189, 0] - access: [] - - target_room: 167 - entrance: 352 - teleporter: [190, 0] - access: [] - - target_room: 171 - entrance: 353 - teleporter: [191, 0] - access: [] -- name: Pazuzu 3F Main Room - id: 170 - game_objects: - - name: "Pazuzu's Tower 3F - Guest Room West Box" - object_id: 0xD0 - type: "Box" - access: [] - - name: "Pazuzu's Tower 3F - Guest Room East Box" - object_id: 0xD1 - type: "Box" - access: [] - - name: "Pazuzu 3F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu3F"] - access: [] - links: - - target_room: 180 - entrance: 356 - teleporter: [192, 0] - access: [] - - target_room: 181 - entrance: 357 - teleporter: [193, 0] - access: [] -- name: Pazuzu 3F Central Island - id: 171 - game_objects: [] - links: - - target_room: 169 - entrance: 360 - teleporter: [194, 0] - access: [] - - target_room: 170 - access: ["DragonClaw"] - - target_room: 172 - access: ["DragonClaw"] -- name: Pazuzu 3F Southern Island - id: 172 - game_objects: - - name: "Pazuzu's Tower 3F - South Ledge Box" - object_id: 0xD2 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 361 - teleporter: [195, 0] - access: [] - - target_room: 171 - access: ["DragonClaw"] -- name: Pazuzu 4F - id: 173 - game_objects: - - name: "Pazuzu's Tower 4F - Elevator West Box" - object_id: 0xD3 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - Elevator East Box" - object_id: 0xD4 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - East Storage Room Chest" - object_id: 0x18 - type: "Chest" - access: [] - - name: "Pazuzu 4F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4FLock"] - access: ["Axe"] - - name: "Pazuzu 4F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 362 - teleporter: [196, 0] - access: [] - - target_room: 184 - entrance: 363 - teleporter: [197, 0] - access: [] - - target_room: 172 - entrance: 364 - teleporter: [198, 0] - access: [] - - target_room: 175 - entrance: 365 - teleporter: [199, 0] - access: [] -- name: Pazuzu 5F Pazuzu Loop - id: 174 - game_objects: - - name: "Pazuzu 5F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu5F"] - access: [] - links: - - target_room: 181 - entrance: 368 - teleporter: [200, 0] - access: [] - - target_room: 182 - entrance: 369 - teleporter: [201, 0] - access: [] -- name: Pazuzu 5F Upper Loop - id: 175 - game_objects: - - name: "Pazuzu's Tower 5F - North Box" - object_id: 0xD5 - type: "Box" - access: [] - - name: "Pazuzu's Tower 5F - South Box" - object_id: 0xD6 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 370 - teleporter: [202, 0] - access: [] - - target_room: 176 - entrance: 371 - teleporter: [203, 0] - access: [] -- name: Pazuzu 6F - id: 176 - game_objects: - - name: "Pazuzu's Tower 6F - Box" - object_id: 0xD7 - type: "Box" - access: [] - - name: "Pazuzu's Tower 6F - Chest" - object_id: 0x19 - type: "Chest" - access: [] - - name: "Pazuzu 6F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6FLock"] - access: ["Bomb", "Axe"] - - name: "Pazuzu 6F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6F"] - access: ["Bomb"] - links: - - target_room: 184 - entrance: 374 - teleporter: [204, 0] - access: [] - - target_room: 175 - entrance: 375 - teleporter: [205, 0] - access: [] - - target_room: 178 - entrance: 376 - teleporter: [206, 0] - access: [] - - target_room: 178 - entrance: 377 - teleporter: [207, 0] - access: [] -- name: Pazuzu 7F Southwest Area - id: 177 - game_objects: [] - links: - - target_room: 182 - entrance: 380 - teleporter: [26, 0] - access: [] - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 7F Rest of the Area - id: 178 - game_objects: [] - links: - - target_room: 177 - access: ["DragonClaw"] - - target_room: 176 - entrance: 381 - teleporter: [27, 0] - access: [] - - target_room: 176 - entrance: 382 - teleporter: [28, 0] - access: [] - - target_room: 179 - access: ["DragonClaw", "Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] -- name: Pazuzu 7F Sky Room - id: 179 - game_objects: - - name: "Pazuzu's Tower 7F - Pazuzu Chest" - object_id: 0x1A - type: "Chest" - access: [] - - name: "Pazuzu" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu"] - access: ["Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] - links: - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 1F to 3F - id: 180 - game_objects: [] - links: - - target_room: 166 - entrance: 385 - teleporter: [29, 0] - access: [] - - target_room: 170 - entrance: 386 - teleporter: [30, 0] - access: [] -- name: Pazuzu 3F to 5F - id: 181 - game_objects: [] - links: - - target_room: 170 - entrance: 387 - teleporter: [40, 0] - access: [] - - target_room: 174 - entrance: 388 - teleporter: [41, 0] - access: [] -- name: Pazuzu 5F to 7F - id: 182 - game_objects: [] - links: - - target_room: 174 - entrance: 389 - teleporter: [38, 0] - access: [] - - target_room: 177 - entrance: 390 - teleporter: [39, 0] - access: [] -- name: Pazuzu 2F to 4F - id: 183 - game_objects: [] - links: - - target_room: 169 - entrance: 391 - teleporter: [21, 0] - access: [] - - target_room: 173 - entrance: 392 - teleporter: [22, 0] - access: [] -- name: Pazuzu 4F to 6F - id: 184 - game_objects: [] - links: - - target_room: 173 - entrance: 393 - teleporter: [2, 0] - access: [] - - target_room: 176 - entrance: 394 - teleporter: [3, 0] - access: [] -- name: Light Temple - id: 185 - game_objects: - - name: "Light Temple - Box" - object_id: 0xD8 - type: "Box" - access: [] - links: - - target_room: 230 - entrance: 395 - teleporter: [19, 6] - access: [] - - target_room: 153 - entrance: 396 - teleporter: [70, 8] - access: ["MobiusCrest"] -- name: Ship Dock - id: 186 - game_objects: - - name: "Ship Dock Access" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipDockAccess"] - access: [] - links: - - target_room: 228 - entrance: 399 - teleporter: [17, 6] - access: [] - - target_room: 162 - entrance: 397 - teleporter: [61, 8] - access: ["MobiusCrest"] -- name: Mac Ship Deck - id: 187 - game_objects: - - name: "Mac Ship Steering Wheel" - object_id: 00 - type: "Trigger" - on_trigger: ["ShipSteeringWheel"] - access: [] - - name: "Mac's Ship Deck - North Box" - object_id: 0xD9 - type: "Box" - access: [] - - name: "Mac's Ship Deck - Center Box" - object_id: 0xDA - type: "Box" - access: [] - - name: "Mac's Ship Deck - South Box" - object_id: 0xDB - type: "Box" - access: [] - links: - - target_room: 229 - entrance: 400 - teleporter: [37, 8] - access: [] - - target_room: 188 - entrance: 401 - teleporter: [50, 8] - access: [] - - target_room: 188 - entrance: 402 - teleporter: [51, 8] - access: [] - - target_room: 188 - entrance: 403 - teleporter: [52, 8] - access: [] - - target_room: 189 - entrance: 404 - teleporter: [53, 8] - access: [] -- name: Mac Ship B1 Outer Ring - id: 188 - game_objects: - - name: "Mac's Ship B1 - Northwest Hook Platform Box" - object_id: 0xE4 - type: "Box" - access: ["DragonClaw"] - - name: "Mac's Ship B1 - Center Hook Platform Box" - object_id: 0xE5 - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 187 - entrance: 405 - teleporter: [208, 0] - access: [] - - target_room: 187 - entrance: 406 - teleporter: [175, 0] - access: [] - - target_room: 187 - entrance: 407 - teleporter: [172, 0] - access: [] - - target_room: 193 - entrance: 408 - teleporter: [88, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B1 Square Room - id: 189 - game_objects: [] - links: - - target_room: 187 - entrance: 409 - teleporter: [141, 0] - access: [] - - target_room: 192 - entrance: 410 - teleporter: [87, 0] - access: [] -- name: Mac Ship B1 Central Corridor - id: 190 - game_objects: - - name: "Mac's Ship B1 - Central Corridor Box" - object_id: 0xE6 - type: "Box" - access: [] - links: - - target_room: 192 - entrance: 413 - teleporter: [86, 0] - access: [] - - target_room: 191 - entrance: 412 - teleporter: [102, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B2 South Corridor - id: 191 - game_objects: [] - links: - - target_room: 190 - entrance: 415 - teleporter: [55, 8] - access: [] - - target_room: 194 - entrance: 414 - teleporter: [57, 1] - access: [] -- name: Mac Ship B2 North Corridor - id: 192 - game_objects: [] - links: - - target_room: 190 - entrance: 416 - teleporter: [56, 8] - access: [] - - target_room: 189 - entrance: 417 - teleporter: [57, 8] - access: [] -- name: Mac Ship B2 Outer Ring - id: 193 - game_objects: - - name: "Mac's Ship B2 - Barrel Room South Box" - object_id: 0xDF - type: "Box" - access: [] - - name: "Mac's Ship B2 - Barrel Room North Box" - object_id: 0xE0 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southwest Room Box" - object_id: 0xE1 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southeast Room Box" - object_id: 0xE2 - type: "Box" - access: [] - links: - - target_room: 188 - entrance: 418 - teleporter: [58, 8] - access: [] -- name: Mac Ship B1 Mac Room - id: 194 - game_objects: - - name: "Mac's Ship B1 - Mac Room Chest" - object_id: 0x1B - type: "Chest" - access: [] - - name: "Captain Mac" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLoaned"] - access: ["CaptainCap"] - links: - - target_room: 191 - entrance: 424 - teleporter: [101, 0] - access: [] -- name: Doom Castle Corridor of Destiny - id: 195 - game_objects: [] - links: - - target_room: 201 - entrance: 428 - teleporter: [84, 0] - access: [] - - target_room: 196 - entrance: 429 - teleporter: [35, 2] - access: [] - - target_room: 197 - entrance: 430 - teleporter: [209, 0] - access: ["StoneGolem"] - - target_room: 198 - entrance: 431 - teleporter: [211, 0] - access: ["StoneGolem", "TwinheadWyvern"] - - target_room: 199 - entrance: 432 - teleporter: [13, 2] - access: ["StoneGolem", "TwinheadWyvern", "Zuh"] -- name: Doom Castle Ice Floor - id: 196 - game_objects: - - name: "Doom Castle 4F - Northwest Room Box" - object_id: 0xE7 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Southwest Room Box" - object_id: 0xE8 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Northeast Room Box" - object_id: 0xE9 - type: "Box" - access: ["Sword"] - - name: "Doom Castle 4F - Southeast Room Box" - object_id: 0xEA - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Stone Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["StoneGolem"] - access: ["Sword", "DragonClaw"] - links: - - target_room: 195 - entrance: 433 - teleporter: [109, 3] - access: [] -- name: Doom Castle Lava Floor - id: 197 - game_objects: - - name: "Doom Castle 5F - North Left Box" - object_id: 0xEB - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - North Right Box" - object_id: 0xEC - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Left Box" - object_id: 0xED - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Right Box" - object_id: 0xEE - type: "Box" - access: ["DragonClaw"] - - name: "Twinhead Wyvern" - object_id: 0 - type: "Trigger" - on_trigger: ["TwinheadWyvern"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 434 - teleporter: [210, 0] - access: [] -- name: Doom Castle Sky Floor - id: 198 - game_objects: - - name: "Doom Castle 6F - West Box" - object_id: 0xEF - type: "Box" - access: [] - - name: "Doom Castle 6F - East Box" - object_id: 0xF0 - type: "Box" - access: [] - - name: "Zuh" - object_id: 0 - type: "Trigger" - on_trigger: ["Zuh"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 435 - teleporter: [212, 0] - access: [] - - target_room: 197 - access: [] -- name: Doom Castle Hero Room - id: 199 - game_objects: - - name: "Doom Castle Hero Chest 01" - object_id: 0xF2 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 02" - object_id: 0xF3 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 03" - object_id: 0xF4 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 04" - object_id: 0xF5 - type: "Chest" - access: [] - links: - - target_room: 200 - entrance: 436 - teleporter: [54, 0] - access: [] - - target_room: 195 - entrance: 441 - teleporter: [110, 3] - access: [] -- name: Doom Castle Dark King Room - id: 200 - game_objects: [] - links: - - target_room: 199 - entrance: 442 - teleporter: [52, 0] - access: [] diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md index a652d4e5adcd..4e093930739d 100644 --- a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md +++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md @@ -1,5 +1,8 @@ # Final Fantasy Mystic Quest +## Game page in other languages: +* [Franįais](/games/Final%20Fantasy%20Mystic%20Quest/info/fr) + ## Where is the options page? The [player options page for this game](../player-options) contains all the options you need to configure and export a diff --git a/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md new file mode 100644 index 000000000000..70c2d938bfc6 --- /dev/null +++ b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md @@ -0,0 +1,36 @@ +# Final Fantasy Mystic Quest + +## Page d'info dans d'autres langues : +* [English](/games/Final%20Fantasy%20Mystic%20Quest/info/en) + +## OÚ se situe la page d'options? + +La [page de configuration](../player-options) contient toutes les options nÊcessaires pour crÊer un fichier de configuration. + +## Qu'est-ce qui est rendu alÊatoire dans ce jeu? + +Outre les objets mÊlangÊs, il y a plusieurs options pour aussi mÊlanger les villes et donjons, les pièces dans les donjons, les tÊlÊporteurs et les champs de bataille. +Il y a aussi plusieurs autres options afin d'ajuster la difficultÊ du jeu et la vitesse d'une partie. + +## Quels objets et emplacements sont mÊlangÊs? + +Les objets normalement reçus des coffres rouges, des PNJ et des champs de bataille sont mÊlangÊs. Vous pouvez aussi +inclure les objets des coffres bruns (qui contiennent normalement des consommables) dans les objets mÊlangÊs. + +## Quels objets peuvent ÃĒtre dans les mondes des autres joueurs? + +Tous les objets qui ont ÊtÊ dÊterminÊs mÊlangÊs dans les options peuvent ÃĒtre placÊs dans d'autres mondes. + +## À quoi ressemblent les objets des autres joueurs dans Final Fantasy Mystic Quest? + +Les emplacements qui Êtaient à l'origine des coffres (rouges ou bruns si ceux-ci sont inclus) apparaÃŽtront comme des coffres. +Les coffres rouges seront des objets utiles ou de progression, alors que les coffres bruns seront des objets de remplissage. +Les pièges peuvent apparaÃŽtre comme des coffres rouges ou bruns. +Lorsque vous ouvrirez un coffre contenant un objet d'un autre joueur, vous recevrez l'icône d'Archipelago et +la boÃŽte de dialogue vous indiquera avoir reçu un "Archipelago Item". + + +## Lorsqu'un joueur reçoit un objet, qu'arrive-t-il? + +Une boÃŽte de dialogue apparaÃŽtra pour vous montrer l'objet que vous avez reçu. Vous ne pourrez pas recevoir d'objet si vous ÃĒtes +en combat, dans la mappemonde ou dans les menus (à l'exception de lorsque vous fermez le menu). diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md index 35d775f1bc9f..77569c93f0c8 100644 --- a/worlds/ffmq/docs/setup_en.md +++ b/worlds/ffmq/docs/setup_en.md @@ -17,6 +17,12 @@ The Archipelago community cannot supply you with this. ## Installation Procedures +### Linux Setup + +1. Download and install [Archipelago](). **The installer + file is located in the assets section at the bottom of the version information. You'll likely be looking for the `.AppImage`.** +2. It is recommended to use either RetroArch or BizHawk if you run on linux, as snes9x-rr isn't compatible. + ### Windows Setup 1. Download and install [Archipelago](). **The installer @@ -75,8 +81,7 @@ Manually launch the SNI Client, and run the patched ROM in your chosen software #### With an emulator -When the client launched automatically, SNI should have also automatically launched in the background. If this is its -first time launching, you may be prompted to allow it to communicate through the Windows Firewall. +If this is the first time SNI launches, you may be prompted to allow it to communicate through the Windows Firewall. ##### snes9x-rr @@ -133,10 +138,10 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor ### Connect to the Archipelago Server -The patch file which launched your client should have automatically connected you to the AP Server. There are a few -reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the -client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it -into the "Server" input field then press enter. +SNI serves as the interface between your emulator and the server. Since you launched it manually, you need to tell it what server to connect to. +If the server is hosted on Archipelago.gg, get the port the server hosts your game on at the top of the game room (last line before the worlds are listed). +In the SNI client, either type `/connect address` (where `address` is the address of the server, for example `/connect archipelago.gg:12345`), or type the address and port on the "Server" input field, then press `Connect`. +If the server is hosted locally, simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press `Connect`. The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". diff --git a/worlds/ffmq/docs/setup_fr.md b/worlds/ffmq/docs/setup_fr.md new file mode 100644 index 000000000000..12ea41c6b3a0 --- /dev/null +++ b/worlds/ffmq/docs/setup_fr.md @@ -0,0 +1,178 @@ +# Final Fantasy Mystic Quest Setup Guide + +## Logiciels requis + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Une solution logicielle ou matÊrielle capable de charger et de lancer des fichiers ROM de SNES + - Un Êmulateur capable d'ÊxÊcuter des scripts Lua + - snes9x-rr de: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html), + - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Ou, + - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matÊrielle + compatible +- Le fichier ROM de la v1.0 ou v1.1 NA de Final Fantasy Mystic Quest obtenu lÊgalement, sÃģrement nommÊ `Final Fantasy - Mystic Quest (U) (V1.0).sfc` ou `Final Fantasy - Mystic Quest (U) (V1.1).sfc` +La communautÊ d'Archipelago ne peut vous fournir avec ce fichier. + +## ProcÊdure d'installation + +### Installation sur Linux + +1. TÊlÊchargez et installez [Archipelago](). +** Le fichier d'installation est situÊ dans la section "assets" dans le bas de la fenÃĒtre d'information de la version. Vous voulez probablement le `.AppImage`** +2. L'utilisation de RetroArch ou BizHawk est recommandÊ pour les utilisateurs linux, puisque snes9x-rr n'est pas compatible. + +### Installation sur Windows + +1. TÊlÊchargez et installez [Archipelago](). +** Le fichier d'installation est situÊ dans la section "assets" dans le bas de la fenÃĒtre d'information de la version.** +2. Si vous utilisez un Êmulateur, il est recommandÊ d'assigner votre Êmulateur capable d'ÊxÊcuter des scripts Lua comme + programme par dÊfaut pour ouvrir vos ROMs. + 1. Extrayez votre dossier d'Êmulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez. + 2. Faites un clic droit sur un fichier ROM et sÊlectionnez **Ouvrir avec...** + 3. Cochez la case à côtÊ de **Toujours utiliser cette application pour ouvrir les fichiers `.sfc`** + 4. Descendez jusqu'en bas de la liste et sÊlectionnez **Rechercher une autre application sur ce PC** + 5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre Êmulateur et choisissez **Ouvrir**. Ce fichier + devrait se trouver dans le dossier que vous avez extrait à la première Êtape. + + +## CrÊer son fichier de configuration (.yaml) + +### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? + +Votre fichier de configuration contient un ensemble d'options de configuration pour indiquer au gÊnÊrateur +comment il devrait gÊnÊrer votre seed. Chaque joueur d'un multiworld devra fournir son propre fichier de configuration. Cela permet +à chaque joueur d'apprÊcier une expÊrience personalisÊe. Les diffÊrents joueurs d'un mÃĒme multiworld +pouront avoir des options de gÊnÊration diffÊrentes. +Vous pouvez lire le [guide pour crÊer un YAML de base](/tutorial/Archipelago/setup/en) en anglais. + +### OÚ est-ce que j'obtiens un fichier de configuration ? + +La [page d'options sur le site](/games/Final%20Fantasy%20Mystic%20Quest/player-options) vous permet de choisir vos +options de gÊnÊration et de les exporter vers un fichier de configuration. +Il vous est aussi possible de trouver le fichier de configuration modèle de Mystic Quest dans votre rÊpertoire d'installation d'Archipelago, +dans le dossier Players/Templates. + +### VÊrifier son fichier de configuration + +Si vous voulez valider votre fichier de configuration pour ÃĒtre sÃģr qu'il fonctionne, vous pouvez le vÊrifier sur la page du +[Validateur de YAML](/mysterycheck). + +## GÊnÊrer une partie pour un joueur + +1. Aller sur la page [GÊnÊration de partie](/games/Final%20Fantasy%20Mystic%20Quest/player-options), configurez vos options, + et cliquez sur le bouton "Generate Game". +2. Il vous sera alors prÊsentÊ une page d'informations sur la seed +3. Cliquez sur le lien "Create New Room". +4. Vous verrez s'afficher la page du server, de laquelle vous pourrez tÊlÊcharger votre fichier patch `.apmq`. +5. Rendez-vous sur le [site FFMQR](https://ffmqrando.net/Archipelago). +Sur cette page, sÊlectionnez votre ROM Final Fantasy Mystic Quest original dans le boÃŽte "ROM", puis votre ficher patch `.apmq` dans la boÃŽte "Load Archipelago Config File". +Cliquez sur "Generate". Un tÊlÊchargement avec votre ROM alÊatoire devrait s'amorcer. +6. Puisque cette partie est à un seul joueur, vous n'avez plus besoin du client Archipelago ni du serveur, sentez-vous libre de les fermer. + +## Rejoindre un MultiWorld + +### Obtenir son patch et crÊer sa ROM + +Quand vous rejoignez un multiworld, il vous sera demandÊ de fournir votre fichier de configuration à celui qui hÊberge la partie ou +s'occupe de la gÊnÊration. Une fois cela fait, l'hôte vous fournira soit un lien pour tÊlÊcharger votre patch, soit un +fichier `.zip` contenant les patchs de tous les joueurs. Votre patch devrait avoir l'extension `.apmq`. + +Allez au [site FFMQR](https://ffmqrando.net/Archipelago) et sÊlectionnez votre ROM Final Fantasy Mystic Quest original dans le boÃŽte "ROM", puis votre ficher patch `.apmq` dans la boÃŽte "Load Archipelago Config File". +Cliquez sur "Generate". Un tÊlÊchargement avec votre ROM alÊatoire devrait s'amorcer. + +Ouvrez le client SNI (sur Windows ArchipelagoSNIClient.exe, sur Linux ouvrez le `.appImage` puis cliquez sur SNI Client), puis ouvrez le ROM tÊlÊchargÊ avec votre Êmulateur choisi. + +### Se connecter au client + +#### Avec un Êmulateur + +Quand le client se lance automatiquement, QUsb2Snes devrait Êgalement se lancer automatiquement en arrière-plan. Si +c'est la première fois qu'il dÊmarre, il vous sera peut-ÃĒtre demandÊ de l'autoriser à communiquer à travers le pare-feu +Windows. + +##### snes9x-rr + +1. Chargez votre ROM si ce n'est pas dÊjà fait. +2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting** +3. Cliquez alors sur **New Lua Script Window...** +4. Dans la nouvelle fenÃĒtre, sÊlectionnez **Browse...** +5. SÊlectionnez le fichier connecteur lua fourni avec votre client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dÊpendemment de si votre emulateur + est 64-bit ou 32-bit. +6. Si vous obtenez une erreur `socket.dll missing` ou une erreur similaire lorsque vous chargez le script lua, vous devez naviguer dans le dossier +contenant le script lua, puis copier le fichier `socket.dll` dans le dossier d'installation de votre emulateur snes9x. + +##### BizHawk + +1. Assurez vous d'avoir le coeur BSNES chargÊ. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant + ces options de menu : + `Config --> Cores --> SNES --> BSNES` + Une fois le coeur changÊ, vous devez redÊmarrer BizHawk. +2. Chargez votre ROM si ce n'est pas dÊjà fait. +3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console** +4. Cliquez sur le bouton pour ouvrir un nouveau script Lua, soit par le bouton avec un icône "Ouvrir un dossier", + en cliquant `Open Script...` dans le menu Script ou en appuyant sur `ctrl-O`. +5. SÊlectionnez le fichier `Connector.lua` inclus avec le client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dÊpendemment de si votre emulateur + est 64-bit ou 32-bit. Notez que les versions les plus rÊcentes de BizHawk ne sont que 64-bit. + +##### RetroArch 1.10.1 ou plus rÊcent + +Vous ne devez faire ces Êtapes qu'une fois. À noter que RetroArch 1.9.x ne fonctionnera pas puisqu'il s'agit d'une version moins rÊcente que 1.10.1. + +1. Entrez dans le menu principal de RetroArch. +2. Allez dans Settings --> User Interface. Activez l'option "Show Advanced Settings". +3. Allez dans Settings --> Network. Activez l'option "Network Commands", qui se trouve sous "Request Device 16". + Laissez le "Network Command Port" à sa valeur par defaut, qui devrait ÃĒtre 55355. + + +![Capture d'Êcran du menu Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Allez dans le Menu Principal --> Online Updater --> Core Downloader. Trouvez et sÊlectionnez "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +Lorsque vous chargez un ROM pour Archipelago, assurez vous de toujours sÊlectionner le coeur **bsnes-mercury**. +Ce sont les seuls coeurs qui permettent à des outils extÊrieurs de lire les donnÊes du ROM. + +#### Avec une solution matÊrielle + +Ce guide suppose que vous avez tÊlÊchargÊ le bon micro-logiciel pour votre appareil. Si ce n'est pas dÊjà le cas, faites +le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent tÊlÊcharger le micro-logiciel appropriÊ +[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut ÃĒtre trouvÊe +[sur cette page](http://usb2snes.com/#supported-platforms). + +1. Fermez votre Êmulateur, qui s'est potentiellement lancÊ automatiquement. +2. Ouvrez votre appareil et chargez le ROM. + +### Se connecter au MultiServer + +Puisque vous avez lancÊ SNI manuellement, vous devrez probablement lui indiquer l'adresse à laquelle il doit se connecter. +Si le serveur est hÊbergÊ sur le site d'Archipelago, vous verrez l'adresse à laquelle vous connecter dans le haut de la page, dernière ligne avant la liste des mondes. +Tapez `/connect adresse` (ou le "adresse" est remplacÊ par l'adresse archipelago, par exemple `/connect archipelago.gg:12345`) dans la boÃŽte de commande au bas de votre client SNI, ou encore Êcrivez l'adresse dans la boÃŽte "server" dans le haut du client, puis cliquez `Connect`. +Si le serveur n'est pas hÊbergÊ sur le site d'Archipelago, demandez à l'hôte l'adresse du serveur, puis tapez `/connect adresse` (ou "adresse" est remplacÊ par l'adresse fourni par l'hôte) ou copiez/collez cette adresse dans le champ "Server" puis appuyez sur "Connect". + +Le client essaiera de vous reconnecter à la nouvelle adresse du serveur, et devrait mentionner "Server Status: +Connected". Si le client ne se connecte pas après quelques instants, il faudra peut-ÃĒtre rafraÃŽchir la page de +l'interface Web. + +### Jouer au jeu + +Une fois que l'interface Web affiche que la SNES et le serveur sont connectÊs, vous ÃĒtes prÃĒt à jouer. FÊlicitations +pour avoir rejoint un multiworld ! + +## HÊberger un MultiWorld + +La mÊthode recommandÊe pour hÊberger une partie est d'utiliser le service d'hÊbergement fourni par +Archipelago. Le processus est relativement simple : + +1. RÊcupÊrez les fichiers de configuration (.yaml) des joueurs. +2. CrÊez une archive zip contenant ces fichiers de configuration. +3. TÊlÊversez l'archive zip sur le lien ci-dessous. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Attendez un moment que la seed soit gÊnÊrÊe. +5. Lorsque la seed est gÊnÊrÊe, vous serez redirigÊ vers une page d'informations "Seed Info". +6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres + joueurs afin qu'ils puissent rÊcupÊrer leurs patchs. +7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez Êgalement + fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quelle personne voulant + observer devrait avoir accès à ce lien. +8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer. diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index e930c4b8d6e9..31d725bff722 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -69,7 +69,7 @@ def forbid(sender: int, receiver: int, items: typing.Set[str]): if (location.player, location.item_rule) in func_cache: location.item_rule = func_cache[location.player, location.item_rule] # empty rule that just returns True, overwrite - elif location.item_rule is location.__class__.item_rule: + elif location.item_rule is Location.item_rule: func_cache[location.player, location.item_rule] = location.item_rule = \ lambda i, sending_blockers = forbid_data[location.player], \ old_rule = location.item_rule: \ @@ -103,7 +103,7 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): old_rule = spot.access_rule # empty rule, replace instead of add - if old_rule is spot.__class__.access_rule: + if old_rule is Location.access_rule or old_rule is Entrance.access_rule: spot.access_rule = rule if combine == "and" else old_rule else: if combine == "and": @@ -115,7 +115,7 @@ def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule # empty rule - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = lambda i: i.name != item or i.player != player else: location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) @@ -135,7 +135,7 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): old_rule = location.item_rule # empty rule, replace instead of add - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = rule if combine == "and" else old_rule else: if combine == "and": diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 37467eeb468e..2197c0708e9c 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -102,10 +102,10 @@ See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `accessibility` determines the level of access to the game the generation will expect you to have in order to reach - your completion goal. This supports `items`, `locations`, and `minimal` and is set to `locations` by default. - * `locations` will guarantee all locations are accessible in your world. + your completion goal. This supports `full`, `items`, and `minimal` and is set to `full` by default. + * `full` will guarantee all locations are accessible in your world. * `items` will guarantee you can acquire all logically relevant items in your world. Some items, such as keys, may - be self-locking. + be self-locking. This value only exists in and affects some worlds. * `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon. diff --git a/worlds/generic/docs/mac_en.md b/worlds/generic/docs/mac_en.md index 2904781862da..76b1ee4a3827 100644 --- a/worlds/generic/docs/mac_en.md +++ b/worlds/generic/docs/mac_en.md @@ -2,8 +2,8 @@ Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal. ## Prerequisite Software Here is a list of software to install and source code to download. -1. Python 3.9 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). - **Python 3.11 is not supported yet.** +1. Python 3.10 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/). + **Python 3.13 is not supported yet.** 2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835). 3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases). 4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases). 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/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index 6e65459851e3..22622cd0e94d 100644 --- a/worlds/generic/docs/setup_en.md +++ b/worlds/generic/docs/setup_en.md @@ -12,7 +12,7 @@ Some steps also assume use of Windows, so may vary with your OS. ## Installing the Archipelago software The most recent public release of Archipelago can be found on GitHub: -[Archipelago Lastest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest). +[Archipelago Latest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest). Run the exe file, and after accepting the license agreement you will be asked which components you would like to install. diff --git a/worlds/heretic/Options.py b/worlds/heretic/Options.py index 75e2257a7336..7d98207b0f8e 100644 --- a/worlds/heretic/Options.py +++ b/worlds/heretic/Options.py @@ -16,14 +16,8 @@ class Goal(Choice): class Difficulty(Choice): """ - Choose the difficulty option. Those match DOOM's difficulty options. - baby (I'm too young to die.) double ammos, half damage, less monsters or strength. - easy (Hey, not too rough.) less monsters or strength. - medium (Hurt me plenty.) Default. - hard (Ultra-Violence.) More monsters or strength. - nightmare (Nightmare!) Monsters attack more rapidly and respawn. - - wet nurse (hou needeth a wet-nurse) - Fewer monsters and more items than medium. Damage taken is halved, and ammo pickups carry twice as much ammo. Any Quartz Flasks and Mystic Urns are automatically used when the player nears death. + Choose the game difficulty. These options match Heretic's skill levels. + wet nurse (Thou needeth a wet-nurse) - Fewer monsters and more items than medium. Damage taken is halved, and ammo pickups carry twice as much ammo. Any Quartz Flasks and Mystic Urns are automatically used when the player nears death. easy (Yellowbellies-r-us) - Fewer monsters and more items than medium. medium (Bringest them oneth) - Completely balanced, this is the standard difficulty level. hard (Thou art a smite-meister) - More monsters and fewer items than medium. @@ -35,6 +29,11 @@ class Difficulty(Choice): option_medium = 2 option_hard = 3 option_black_plague = 4 + alias_wn = 0 + alias_yru = 1 + alias_bto = 2 + alias_sm = 3 + alias_bp = 4 default = 2 @@ -104,7 +103,7 @@ class StartWithMapScrolls(Toggle): class ResetLevelOnDeath(DefaultOnToggle): """When dying, levels are reset and monsters respawned. But inventory and checks are kept. Turning this setting off is considered easy mode. Good for new players that don't know the levels well.""" - display_message="Reset level on death" + display_name = "Reset Level on Death" class CheckSanity(Toggle): diff --git a/worlds/heretic/docs/setup_en.md b/worlds/heretic/docs/setup_en.md index 41b7fdab8078..5985dbb0992a 100644 --- a/worlds/heretic/docs/setup_en.md +++ b/worlds/heretic/docs/setup_en.md @@ -15,7 +15,7 @@ You can find the folder in steam by finding the game in your library, right clicking it and choosing *Manage→Browse Local Files*. The WAD file is in the `/base/` folder. -## Joining a MultiWorld Game +## Joining a MultiWorld Game (via Launcher) 1. Launch apdoom-launcher.exe 2. Choose Heretic in the dropdown @@ -26,6 +26,23 @@ To continue a game, follow the same connection steps. Connecting with a different seed won't erase your progress in other seeds. +## Joining a MultiWorld Game (via command line) + +1. In your command line, navigate to the directory where APDOOM is installed. +2. Run `crispy-apheretic -apserver -applayer `, where: + - `` is the Archipelago server address, e.g. "`archipelago.gg:38281`" + - `` is your slot name; if it contains spaces, surround it with double quotes + - If the server has a password, add `-password`, followed by the server password +3. Enjoy! + +Optionally, you can override some randomization settings from the command line: +- `-apmonsterrando 0` will disable monster rando. +- `-apitemrando 0` will disable item rando. +- `-apmusicrando 0` will disable music rando. +- `-apresetlevelondeath 0` will disable resetting the level on death. +- `-apdeathlinkoff` will force DeathLink off if it's enabled. +- `-skill <1-5>` changes the game difficulty, from 1 (thou needeth a wet-nurse) to 5 (black plague possesses thee) + ## Archipelago Text Client We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send. diff --git a/worlds/hk/Extractor.py b/worlds/hk/Extractor.py index 61fabc4da0d9..866608489ec2 100644 --- a/worlds/hk/Extractor.py +++ b/worlds/hk/Extractor.py @@ -9,11 +9,7 @@ import jinja2 -try: - from ast import unparse -except ImportError: - # Py 3.8 and earlier compatibility module - from astunparse import unparse +from ast import unparse from Utils import get_text_between diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py index 8515465826a5..a2b7c06d62a6 100644 --- a/worlds/hk/Items.py +++ b/worlds/hk/Items.py @@ -61,6 +61,7 @@ class HKItemData(NamedTuple): "VesselFragments": lookup_type_to_names["Vessel"], "WhisperingRoots": lookup_type_to_names["Root"], "WhiteFragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"}, + "DreamNails": {"Dream_Nail", "Dream_Gate", "Awoken_Dream_Nail"}, }) item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash'] item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'} diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 38be2cd794a1..0dc38e744e50 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -1,10 +1,12 @@ import typing import re +from dataclasses import make_dataclass + from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms from schema import And, Schema, Optional -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -75,7 +77,7 @@ "RandomizeLoreTablets": "Randomize Lore items into the itempool, one per Lore Tablet, and place randomized item " "grants on the tablets themselves.\n You must still read the tablet to get the item.", "PreciseMovement": "Places skips into logic which require extremely precise player movement, possibly without " - "movement skills such as\n dash or hook.", + "movement skills such as\n dash or claw.", "ProficientCombat": "Places skips into logic which require proficient combat, possibly with limited items.", "BackgroundObjectPogos": "Places skips into logic for locations which are reachable via pogoing off of " "background objects.", @@ -292,15 +294,40 @@ def get_costs(self, random_source: Random) -> typing.List[int]: return charms +class CharmCost(Range): + range_end = 6 + + class PlandoCharmCosts(OptionDict): """Allows setting a Charm's Notch costs directly, mapping {name: cost}. This is set after any random Charm Notch costs, if applicable.""" display_name = "Charm Notch Cost Plando" valid_keys = frozenset(charm_names) schema = Schema({ - Optional(name): And(int, lambda n: 6 >= n >= 0) for name in charm_names + Optional(name): And(int, lambda n: 6 >= n >= 0, error="Charm costs must be integers in the range 0-6.") for name in charm_names }) + def __init__(self, value): + # To handle keys of random like other options, create an option instance from their values + # Additionally a vanilla keyword is added to plando individual charms to vanilla costs + # and default is disabled so as to not cause confusion + self.value = {} + for key, data in value.items(): + if isinstance(data, str): + if data.lower() == "vanilla" and key in self.valid_keys: + self.value[key] = vanilla_costs[charm_names.index(key)] + continue + elif data.lower() == "default": + # default is too easily confused with vanilla but actually 0 + # skip CharmCost resolution to fail schema afterwords + self.value[key] = data + continue + try: + self.value[key] = CharmCost.from_any(data).value + except ValueError as ex: + # will fail schema afterwords + self.value[key] = data + def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]: for name, cost in self.value.items(): charm_costs[charm_names.index(name)] = cost @@ -403,9 +430,20 @@ class Goal(Choice): option_radiance = 3 option_godhome = 4 option_godhome_flower = 5 + option_grub_hunt = 6 default = 0 +class GrubHuntGoal(NamedRange): + """The amount of grubs required to finish Grub Hunt. + On 'All' any grubs from item links replacements etc. will be counted""" + display_name = "Grub Hunt Goal" + range_start = 1 + range_end = 46 + special_range_names = {"all": -1} + default = 46 + + class WhitePalace(Choice): """ Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be @@ -520,7 +558,7 @@ class CostSanityHybridChance(Range): **{ option.__name__: option for option in ( - StartLocation, Goal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, + StartLocation, Goal, GrubHuntGoal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, @@ -538,3 +576,5 @@ class CostSanityHybridChance(Range): }, **cost_sanity_weights } + +HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,)) diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index a3c7e13cf02b..e162e1dfa81c 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -49,3 +49,42 @@ def set_rules(hk_world: World): if term == "GEO": # No geo logic! continue add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) + + +def _hk_nail_combat(state, player) -> bool: + return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) + + +def _hk_can_beat_thk(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and _hk_nail_combat(state, player) + and ( + state.has_any({'LEFTDASH', 'RIGHTDASH'}, player) + or state._hk_option(player, 'ProficientCombat') + ) + and state.has('FOCUS', player) + ) + + +def _hk_siblings_ending(state, player) -> bool: + return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3) + + +def _hk_can_beat_radiance(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and _hk_nail_combat(state, player) + and state.has('WHITEFRAGMENT', player, 3) + and state.has('DREAMNAIL', player) + and ( + (state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player)) + or state.has('WINGS', player) + ) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and ( + (state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2)) # Both Shade Cloaks + or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player)) # or Dive + ) + ) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 78287305df5f..81d939dcf1ea 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,21 +5,32 @@ from copy import deepcopy import itertools import operator +from collections import defaultdict, Counter logger = logging.getLogger("Hollow Knight") from .Items import item_table, lookup_type_to_names, item_name_groups from .Regions import create_regions -from .Rules import set_rules, cost_terms +from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option + shop_to_option, HKOptions, GrubHuntGoal from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names -from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification +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", @@ -123,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=" @@ -142,7 +164,9 @@ class HKWorld(World): As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. """ # from https://www.hollowknight.com game: str = "Hollow Knight" - option_definitions = hollow_knight_options + options_dataclass = HKOptions + options: HKOptions + settings: typing.ClassVar[HollowKnightSettings] web = HKWeb() @@ -154,40 +178,42 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} + grub_count: int - def __init__(self, world, player): - super(HKWorld, self).__init__(world, player) + def __init__(self, multiworld, player): + super(HKWorld, self).__init__(multiworld, player) self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = { location: list() for location in multi_locations } self.ranges = {} self.created_shop_items = 0 self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) + self.grub_count = 0 def generate_early(self): - world = self.multiworld - charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) - self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) - # world.exclude_locations[self.player].value.update(white_palace_locations) + options = self.options + charm_costs = options.RandomCharmCosts.get_costs(self.random) + self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs) + # options.exclude_locations.value.update(white_palace_locations) for term, data in cost_terms.items(): - mini = getattr(world, f"Minimum{data.option}Price")[self.player] - maxi = getattr(world, f"Maximum{data.option}Price")[self.player] + mini = getattr(options, f"Minimum{data.option}Price") + maxi = getattr(options, f"Maximum{data.option}Price") # if minimum > maximum, set minimum to maximum mini.value = min(mini.value, maxi.value) self.ranges[term] = mini.value, maxi.value - world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key], + self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key], True, None, "Event", self.player)) def white_palace_exclusions(self): exclusions = set() - wp = self.multiworld.WhitePalace[self.player] + wp = self.options.WhitePalace if wp <= WhitePalace.option_nopathofpain: exclusions.update(path_of_pain_locations) if wp <= WhitePalace.option_kingfragment: exclusions.update(white_palace_checks) if wp == WhitePalace.option_exclude: exclusions.add("King_Fragment") - if self.multiworld.RandomizeCharms[self.player]: + if self.options.RandomizeCharms: # If charms are randomized, this will be junk-filled -- so transitions and events are not progression exclusions.update(white_palace_transitions) exclusions.update(white_palace_events) @@ -200,12 +226,12 @@ def create_regions(self): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower, Goal.option_any]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) # Link regions - for event_name in all_event_names: + for event_name in sorted(all_event_names): #if event_name in wp_exclusions: # continue loc = HKLocation(self.player, event_name, None, menu_region) @@ -230,12 +256,12 @@ def create_items(self): pool: typing.List[HKItem] = [] wp_exclusions = self.white_palace_exclusions() junk_replace: typing.Set[str] = set() - if self.multiworld.RemoveSpellUpgrades[self.player]: + if self.options.RemoveSpellUpgrades: junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark")) randomized_starting_items = set() for attr, items in randomizable_starting_items.items(): - if getattr(self.multiworld, attr)[self.player]: + if getattr(self.options, attr): randomized_starting_items.update(items) # noinspection PyShadowingNames @@ -257,7 +283,7 @@ def _add(item_name: str, location_name: str, randomized: bool): if item_name in junk_replace: item_name = self.get_filler_item_name() - item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name) + item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name) if location_name == "Start": if item_name in randomized_starting_items: @@ -281,56 +307,56 @@ def _add(item_name: str, location_name: str, randomized: bool): location.progress_type = LocationProgressType.EXCLUDED for option_key, option in hollow_knight_randomize_options.items(): - randomized = getattr(self.multiworld, option_key)[self.player] - if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]): + randomized = getattr(self.options, option_key) + if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]): continue for item_name, location_name in zip(option.items, option.locations): if item_name in junk_replace: item_name = self.get_filler_item_name() - if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \ - (item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]): + if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \ + (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak): _add("Left_" + item_name, location_name, randomized) _add("Right_" + item_name, "Split_" + location_name, randomized) continue - if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]: + if item_name == "Mantis_Claw" and self.options.SplitMantisClaw: _add("Left_" + item_name, "Left_" + location_name, randomized) _add("Right_" + item_name, "Right_" + location_name, randomized) continue - if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]: - if self.multiworld.random.randint(0, 1): + if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak: + if self.random.randint(0, 1): item_name = "Left_Mothwing_Cloak" else: item_name = "Right_Mothwing_Cloak" - if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]: + if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms: _add("Grimmchild1", location_name, randomized) continue _add(item_name, location_name, randomized) - if self.multiworld.RandomizeElevatorPass[self.player]: + if self.options.RandomizeElevatorPass: randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) for shop, locations in self.created_multi_locations.items(): - for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value): - loc = self.create_location(shop) + for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): + self.create_location(shop) unfilled_locations += 1 # Balance the pool item_count = len(pool) - additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value) + additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value) # Add additional shop items, as needed. if additional_shop_items > 0: shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) - if not self.multiworld.EggShopSlots[self.player].value: # No eggshop, so don't place items there + if not self.options.EggShopSlots: # No eggshop, so don't place items there shops.remove('Egg_Shop') if shops: for _ in range(additional_shop_items): - shop = self.multiworld.random.choice(shops) - loc = self.create_location(shop) + shop = self.random.choice(shops) + self.create_location(shop) unfilled_locations += 1 if len(self.created_multi_locations[shop]) >= 16: shops.remove(shop) @@ -355,7 +381,7 @@ def sort_shops_by_cost(self): loc.costs = costs def apply_costsanity(self): - setting = self.multiworld.CostSanity[self.player].value + setting = self.options.CostSanity.value if not setting: return # noop @@ -369,10 +395,10 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]: return {k: v for k, v in weights.items() if v} - random = self.multiworld.random - hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value + random = self.random + hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value weights = { - data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value + data.term: getattr(self.options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() } weights_geoless = dict(weights) @@ -427,51 +453,100 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]: location.sort_costs() def set_rules(self): - world = self.multiworld + multiworld = self.multiworld player = self.player - goal = world.Goal[player] + goal = self.options.Goal if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player) elif goal == Goal.option_godhome: - world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) + multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: - world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + elif goal == Goal.option_grub_hunt: + pass # will set in stage_pre_fill() else: # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \ + _hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player) set_rules(self) + @classmethod + def stage_pre_fill(cls, multiworld: "MultiWorld"): + def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]): + world = multiworld.worlds[player] + + if world.options.Goal == "grub_hunt": + multiworld.completion_condition[player] = grub_rule + else: + old_rule = multiworld.completion_condition[player] + multiworld.completion_condition[player] = lambda state: old_rule(state) and grub_rule(state) + + worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] + if worlds: + grubs = [item for item in multiworld.get_items() if item.name == "Grub"] + all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + + if all_grub_players: + group_lookup = defaultdict(set) + for group_id, group in multiworld.groups.items(): + for player in group["players"]: + group_lookup[group_id].add(player) + + grub_count_per_player = Counter() + per_player_grubs_per_player = defaultdict(Counter) + + for grub in grubs: + player = grub.player + if player in group_lookup: + for real_player in group_lookup[player]: + per_player_grubs_per_player[real_player][player] += 1 + else: + per_player_grubs_per_player[player][player] += 1 + + if grub.location and grub.location.player in group_lookup.keys(): + # will count the item linked grub instead + pass + elif player in group_lookup: + for real_player in group_lookup[player]: + grub_count_per_player[real_player] += 1 + else: + # for non-linked grubs + grub_count_per_player[player] += 1 + + for player, count in grub_count_per_player.items(): + multiworld.worlds[player].grub_count = count + + for player, grub_player_count in per_player_grubs_per_player.items(): + if player in all_grub_players: + set_goal(player, lambda state, g=grub_player_count: all(state.has("Grub", owner, count) for owner, count in g.items())) + + for world in worlds: + if world.player not in all_grub_players: + world.grub_count = world.options.GrubHuntGoal.value + player = world.player + set_goal(player, lambda state, p=player, c=world.grub_count: state.has("Grub", p, c)) + def fill_slot_data(self): slot_data = {} options = slot_data["options"] = {} - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] + 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.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646) - - # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.multiworld.CostSanity[self.player]: - 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] - } + slot_data["seed"] = self.random.randint(-2147483647, 2147483646) # HKAP 0.1.0 and later cost data. location_costs = {} @@ -483,6 +558,10 @@ def fill_slot_data(self): slot_data["notch_costs"] = self.charm_costs + 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: @@ -498,7 +577,7 @@ def create_location(self, name: str, vanilla=False) -> HKLocation: basename = name if name in shop_cost_types: costs = { - term: self.multiworld.random.randint(*self.ranges[term]) + term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[name] } elif name in vanilla_location_costs: @@ -512,7 +591,7 @@ def create_location(self, name: str, vanilla=False) -> HKLocation: region = self.multiworld.get_region("Menu", self.player) - if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]: + if vanilla and not self.options.AddUnshuffledLocations: loc = HKLocation(self.player, name, None, region, costs=costs, vanilla=vanilla, basename=basename) @@ -540,11 +619,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: @@ -554,31 +633,32 @@ def remove(self, state, item: HKItem) -> bool: for effect_name, effect_value in item_effects.get(item.name, {}).items(): if state.prog_items[item.player][effect_name] == effect_value: del state.prog_items[item.player][effect_name] - state.prog_items[item.player][effect_name] -= effect_value + else: + state.prog_items[item.player][effect_name] -= effect_value return change @classmethod - def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle): - hk_players = world.get_game_players(cls.game) + def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle): + hk_players = multiworld.get_game_players(cls.game) spoiler_handle.write('\n\nCharm Notches:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] for charm_number, cost in enumerate(hk_world.charm_costs): spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}") spoiler_handle.write('\n\nShop Prices:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] - if world.CostSanity[player].value: + if hk_world.options.CostSanity: for loc in sorted( ( - loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player))) + loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player))) if loc.costs ), key=operator.attrgetter('name') ): @@ -602,15 +682,15 @@ def get_filler_item_name(self) -> str: 'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests', 'RandomizeRancidEggs' ): - if getattr(self.multiworld, group): + if getattr(self.options, group): fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in exclusions) self.cached_filler_items[self.player] = fillers - return self.multiworld.random.choice(self.cached_filler_items[self.player]) + return self.random.choice(self.cached_filler_items[self.player]) -def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region: - ret = Region(name, player, world) +def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region: + ret = Region(name, player, multiworld) if location_names: for location in location_names: loc_id = HKWorld.location_name_to_id.get(location, None) @@ -683,42 +763,7 @@ def _hk_notches(self, player: int, *notches: int) -> int: return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches) def _hk_option(self, player: int, option_name: str) -> int: - return getattr(self.multiworld, option_name)[player].value + return getattr(self.multiworld.worlds[player].options, option_name).value def _hk_start(self, player, start_location: str) -> bool: - return self.multiworld.StartLocation[player] == start_location - - def _hk_nail_combat(self, player: int) -> bool: - return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) - - def _hk_can_beat_thk(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and self._hk_nail_combat(player) - and ( - self.has_any({'LEFTDASH', 'RIGHTDASH'}, player) - or self._hk_option(player, 'ProficientCombat') - ) - and self.has('FOCUS', player) - ) - - def _hk_siblings_ending(self, player: int) -> bool: - return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3) - - def _hk_can_beat_radiance(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and self._hk_nail_combat(player) - and self.has('WHITEFRAGMENT', player, 3) - and self.has('DREAMNAIL', player) - and ( - (self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player)) - or self.has('WINGS', player) - ) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and ( - (self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2)) # Both Shade Cloaks - or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player)) # or Dive - ) - ) + return self.multiworld.worlds[player].options.StartLocation == start_location 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/hk/requirements.txt b/worlds/hk/requirements.txt deleted file mode 100644 index 1b410ffb2aed..000000000000 --- a/worlds/hk/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -astunparse>=1.6.3; python_version <= '3.8' \ No newline at end of file diff --git a/worlds/hk/test/__init__.py b/worlds/hk/test/__init__.py new file mode 100644 index 000000000000..c41d20127fcc --- /dev/null +++ b/worlds/hk/test/__init__.py @@ -0,0 +1,62 @@ +import typing +from argparse import Namespace +from BaseClasses import CollectionState, MultiWorld +from Options import ItemLinks +from test.bases import WorldTestBase +from worlds.AutoWorld import AutoWorldRegister, call_all +from .. import HKWorld + + +class linkedTestHK(): + run_default_tests = False + game = "Hollow Knight" + world: HKWorld + expected_grubs: int + item_link_group: typing.List[typing.Dict[str, typing.Any]] + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any(self.item_link_group), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": False, + "replacement_item": "One_Geo", + }]) + }) + return args + + def world_setup(self) -> None: + """ + Create a multiworld with two players that share an itemlink + """ + self.multiworld = MultiWorld(2) + self.multiworld.game = {1: self.game, 2: self.game} + self.multiworld.player_name = {1: "Linker 1", 2: "Linker 2"} + self.multiworld.set_seed() + args = Namespace() + options_dataclass = AutoWorldRegister.world_types[self.game].options_dataclass + for name, option in options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(self.options.get(name, option.default)), + 2: option.from_any(self.options.get(name, option.default)) + }) + args = self.setup_item_links(args) + self.multiworld.set_options(args) + self.multiworld.set_item_links() + # groups get added to state during its constructor so this has to be after item links are set + self.multiworld.state = CollectionState(self.multiworld) + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") + for step in gen_steps: + call_all(self.multiworld, step) + # link the items together and stop at prefill + self.multiworld.link_items() + self.multiworld._all_state = None + call_all(self.multiworld, "pre_fill") + + self.world = self.multiworld.worlds[self.player] + + def test_grub_count(self) -> None: + assert self.world.grub_count == self.expected_grubs, \ + f"Expected {self.expected_grubs} but found {self.world.grub_count}" diff --git a/worlds/hk/test/test_grub_count.py b/worlds/hk/test/test_grub_count.py new file mode 100644 index 000000000000..dba15b614dd9 --- /dev/null +++ b/worlds/hk/test/test_grub_count.py @@ -0,0 +1,165 @@ +from . import linkedTestHK, WorldTestBase +from Options import ItemLinks + + +class test_grubcount_limited(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": 20, + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 20 + + +class test_grubcount_default(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 46 + + +class test_grubcount_all_unlinked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + item_link_group = [] + expected_grubs = 46 + + +class test_grubcount_all_linked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 46 + 23 + + +class test_replacement_only(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 18 # the count of grubs + skills removed from item links + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_replacement_only_unlinked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 9 # Player1s replacement Grubs + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_ignore_others(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + # player2 has more than 46 grubs but they are unlinked so player1s grubs are vanilla + expected_grubs = 46 + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "One_Geo", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_replacement_only_linked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 9 # Player2s linkreplacement grubs + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "One_Geo", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]) + }) + return args 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..1b5acbe97a3c 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(player=self.player, player_name=self.player_name) + 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 new file mode 100644 index 000000000000..e69de29bb2d1 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 65% rename from worlds/kdl3/Regions.py rename to worlds/kdl3/regions.py index 407dcf9680f4..af5208d365f0 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/regions.py @@ -1,61 +1,63 @@ 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} - room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) +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__, "data/Rooms.json")) rooms: Dict[str, KDL3Room] = dict() for room_entry in room_data: room = KDL3Room(room_entry["name"], world.player, world.multiworld, None, room_entry["level"], @@ -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..741ea0083027 --- /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__, "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__, "data/APConsumable.bsdiff4") + patched = bytearray(bsdiff4.patch(bytes(patched), patch)) + if stars: + patch = get_data(__name__, "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__, "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 new file mode 100644 index 000000000000..33fba85f6c54 --- /dev/null +++ b/worlds/kh1/Client.py @@ -0,0 +1,300 @@ +from __future__ import annotations +import os +import json +import sys +import asyncio +import shutil +import logging +import re +import time +from calendar import timegm + +import ModuleUpdate +ModuleUpdate.update() + +import Utils +death_link = False +item_num = 1 + +logger = logging.getLogger("Client") + +if __name__ == "__main__": + Utils.init_logging("KH1Client", exception_logger="Client") + +from NetUtils import NetworkItem, ClientStatus +from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \ + CommonContext, server_loop + + +def check_stdin() -> None: + if Utils.is_windows and sys.stdin: + 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 + if death_link: + death_link = False + self.output(f"Death Link turned off") + 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 + game = "Kingdom Hearts" + items_handling = 0b111 # full remote + + def __init__(self, server_address, password): + super(KH1Context, self).__init__(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") + else: + self.game_communication_path = os.path.expandvars(r"$HOME/KH1FM") + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + for root, dirs, files in os.walk(self.game_communication_path): + for file in files: + if file.find("obtain") <= -1: + os.remove(root+"/"+file) + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(KH1Context, self).server_auth(password_requested) + await self.get_username() + await self.send_connect() + + async def connection_closed(self): + await super(KH1Context, self).connection_closed() + for root, dirs, files in os.walk(self.game_communication_path): + for file in files: + if file.find("obtain") <= -1: + os.remove(root + "/" + file) + global item_num + item_num = 1 + + @property + def endpoints(self): + if self.server: + return [self.server] + else: + return [] + + async def shutdown(self): + await super(KH1Context, self).shutdown() + for root, dirs, files in os.walk(self.game_communication_path): + for file in files: + if file.find("obtain") <= -1: + os.remove(root+"/"+file) + global item_num + item_num = 1 + + def on_package(self, cmd: str, args: dict): + if cmd in {"Connected"}: + if not os.path.exists(self.game_communication_path): + os.makedirs(self.game_communication_path) + for ss in self.checked_locations: + filename = f"send{ss}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + 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])) + f.close() + + ###Support Legacy Games + if "Required Reports" in list(args['slot_data'].keys()) and "required_reports_eotw" not in list(args['slot_data'].keys()): + reports_required = args['slot_data']["Required Reports"] + with open(os.path.join(self.game_communication_path, "required_reports.cfg"), 'w') as f: + f.write(str(reports_required)) + f.close() + ###End Support Legacy Games + + #End Handle Slot Data + + if cmd in {"ReceivedItems"}: + start_index = args["index"] + if start_index != len(self.items_received): + global item_num + for item in args['items']: + found = False + item_filename = f"AP_{str(item_num)}.item" + for filename in os.listdir(self.game_communication_path): + if filename == item_filename: + found = True + if not found: + with open(os.path.join(self.game_communication_path, item_filename), 'w') as f: + f.write(str(NetworkItem(*item).item) + "\n" + str(NetworkItem(*item).location) + "\n" + str(NetworkItem(*item).player)) + f.close() + item_num = item_num + 1 + + if cmd in {"RoomUpdate"}: + if "checked_locations" in args: + for ss in self.checked_locations: + filename = f"send{ss}" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.close() + + if cmd in {"PrintJSON"} and "type" in args: + if args["type"] == "ItemSend": + item = args["item"] + networkItem = NetworkItem(*item) + recieverID = args["receiving"] + senderID = networkItem.player + locationID = networkItem.location + if recieverID != self.slot and senderID == self.slot: + itemName = self.item_names.lookup_in_slot(networkItem.item, recieverID) + itemCategory = networkItem.flags + recieverName = self.player_names[recieverID] + filename = "sent" + with open(os.path.join(self.game_communication_path, filename), 'w') as f: + f.write( + re.sub('[^A-Za-z0-9 ]+', '',str(itemName))[:15] + "\n" + + re.sub('[^A-Za-z0-9 ]+', '',str(recieverName))[:6] + "\n" + + str(itemCategory) + "\n" + + str(locationID)) + f.close() + + def on_deathlink(self, data: dict[str, object]): + self.last_death_link = max(data["time"], self.last_death_link) + text = data.get("cause", "") + if text: + logger.info(f"DeathLink: {text}") + else: + logger.info(f"DeathLink: Received from {data['source']}") + with open(os.path.join(self.game_communication_path, 'dlreceive'), 'w') as f: + f.write(str(int(data["time"]))) + f.close() + + def run_gui(self): + """Import kivy UI system and start running it as self.ui_task.""" + from kvui import GameManager + + class KH1Manager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago KH1 Client" + + self.ui = KH1Manager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +async def game_watcher(ctx: KH1Context): + from .Locations import lookup_id_to_name + while not ctx.exit_event.is_set(): + global death_link + if death_link and "DeathLink" not in ctx.tags: + await ctx.update_death_link(death_link) + if not death_link and "DeathLink" in ctx.tags: + await ctx.update_death_link(death_link) + if ctx.syncing == True: + sync_msg = [{'cmd': 'Sync'}] + if ctx.locations_checked: + sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) + await ctx.send_msgs(sync_msg) + ctx.syncing = False + sending = [] + victory = False + for root, dirs, files in os.walk(ctx.game_communication_path): + for file in files: + if file.find("send") > -1: + st = file.split("send", -1)[1] + if st != "nil": + sending = sending+[(int(st))] + if file.find("victory") > -1: + victory = True + if file.find("dlsend") > -1 and "DeathLink" in ctx.tags: + st = file.split("dlsend", -1)[1] + if st != "nil": + 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: + 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) + if not ctx.finished_game and victory: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + await asyncio.sleep(0.1) + + +def launch(): + async def main(args): + ctx = KH1Context(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + progression_watcher = asyncio.create_task( + game_watcher(ctx), name="KH1ProgressionWatcher") + + await ctx.exit_event.wait() + ctx.server_address = None + + await progression_watcher + + await ctx.shutdown() + + import colorama + + parser = get_base_parser(description="KH1 Client, for text interfacing.") + + args, rest = parser.parse_known_args() + colorama.init() + asyncio.run(main(args)) + colorama.deinit() diff --git a/worlds/kh1/Items.py b/worlds/kh1/Items.py new file mode 100644 index 000000000000..bac98a9b3284 --- /dev/null +++ b/worlds/kh1/Items.py @@ -0,0 +1,532 @@ +from typing import Dict, NamedTuple, Optional, Set + +from BaseClasses import Item, ItemClassification + + +class KH1Item(Item): + game: str = "Kingdom Hearts" + + +class KH1ItemData(NamedTuple): + category: str + code: int + classification: ItemClassification = ItemClassification.filler + max_quantity: int = 1 + weight: int = 1 + + +def get_items_by_category(category: str) -> Dict[str, KH1ItemData]: + item_dict: Dict[str, KH1ItemData] = {} + for name, data in item_table.items(): + if data.category == category: + item_dict.setdefault(name, data) + + return item_dict + + +item_table: Dict[str, KH1ItemData] = { + "Victory": KH1ItemData("VIC", code = 264_0000, classification = ItemClassification.progression, ), + "Potion": KH1ItemData("Item", code = 264_1001, classification = ItemClassification.filler, ), + "Hi-Potion": KH1ItemData("Item", code = 264_1002, classification = ItemClassification.filler, ), + "Ether": KH1ItemData("Item", code = 264_1003, classification = ItemClassification.filler, ), + "Elixir": KH1ItemData("Item", code = 264_1004, classification = ItemClassification.filler, ), + #"B05": KH1ItemData("Item", code = 264_1005, classification = ItemClassification.filler, ), + "Mega-Potion": KH1ItemData("Item", code = 264_1006, classification = ItemClassification.filler, ), + "Mega-Ether": KH1ItemData("Item", code = 264_1007, classification = ItemClassification.filler, ), + "Megalixir": KH1ItemData("Item", code = 264_1008, classification = ItemClassification.filler, ), + #"Fury Stone": KH1ItemData("Synthesis", code = 264_1009, classification = ItemClassification.filler, ), + #"Power Stone": KH1ItemData("Synthesis", code = 264_1010, classification = ItemClassification.filler, ), + #"Energy Stone": KH1ItemData("Synthesis", code = 264_1011, classification = ItemClassification.filler, ), + #"Blazing Stone": KH1ItemData("Synthesis", code = 264_1012, classification = ItemClassification.filler, ), + #"Frost Stone": KH1ItemData("Synthesis", code = 264_1013, classification = ItemClassification.filler, ), + #"Lightning Stone": KH1ItemData("Synthesis", code = 264_1014, classification = ItemClassification.filler, ), + #"Dazzling Stone": KH1ItemData("Synthesis", code = 264_1015, classification = ItemClassification.filler, ), + #"Stormy Stone": KH1ItemData("Synthesis", code = 264_1016, classification = ItemClassification.filler, ), + "Protect Chain": KH1ItemData("Accessory", code = 264_1017, classification = ItemClassification.useful, ), + "Protera Chain": KH1ItemData("Accessory", code = 264_1018, classification = ItemClassification.useful, ), + "Protega Chain": KH1ItemData("Accessory", code = 264_1019, classification = ItemClassification.useful, ), + "Fire Ring": KH1ItemData("Accessory", code = 264_1020, classification = ItemClassification.useful, ), + "Fira Ring": KH1ItemData("Accessory", code = 264_1021, classification = ItemClassification.useful, ), + "Firaga Ring": KH1ItemData("Accessory", code = 264_1022, classification = ItemClassification.useful, ), + "Blizzard Ring": KH1ItemData("Accessory", code = 264_1023, classification = ItemClassification.useful, ), + "Blizzara Ring": KH1ItemData("Accessory", code = 264_1024, classification = ItemClassification.useful, ), + "Blizzaga Ring": KH1ItemData("Accessory", code = 264_1025, classification = ItemClassification.useful, ), + "Thunder Ring": KH1ItemData("Accessory", code = 264_1026, classification = ItemClassification.useful, ), + "Thundara Ring": KH1ItemData("Accessory", code = 264_1027, classification = ItemClassification.useful, ), + "Thundaga Ring": KH1ItemData("Accessory", code = 264_1028, classification = ItemClassification.useful, ), + "Ability Stud": KH1ItemData("Accessory", code = 264_1029, classification = ItemClassification.useful, ), + "Guard Earring": KH1ItemData("Accessory", code = 264_1030, classification = ItemClassification.useful, ), + "Master Earring": KH1ItemData("Accessory", code = 264_1031, classification = ItemClassification.useful, ), + "Chaos Ring": KH1ItemData("Accessory", code = 264_1032, classification = ItemClassification.useful, ), + "Dark Ring": KH1ItemData("Accessory", code = 264_1033, classification = ItemClassification.useful, ), + "Element Ring": KH1ItemData("Accessory", code = 264_1034, classification = ItemClassification.useful, ), + "Three Stars": KH1ItemData("Accessory", code = 264_1035, classification = ItemClassification.useful, ), + "Power Chain": KH1ItemData("Accessory", code = 264_1036, classification = ItemClassification.useful, ), + "Golem Chain": KH1ItemData("Accessory", code = 264_1037, classification = ItemClassification.useful, ), + "Titan Chain": KH1ItemData("Accessory", code = 264_1038, classification = ItemClassification.useful, ), + "Energy Bangle": KH1ItemData("Accessory", code = 264_1039, classification = ItemClassification.useful, ), + "Angel Bangle": KH1ItemData("Accessory", code = 264_1040, classification = ItemClassification.useful, ), + "Gaia Bangle": KH1ItemData("Accessory", code = 264_1041, classification = ItemClassification.useful, ), + "Magic Armlet": KH1ItemData("Accessory", code = 264_1042, classification = ItemClassification.useful, ), + "Rune Armlet": KH1ItemData("Accessory", code = 264_1043, classification = ItemClassification.useful, ), + "Atlas Armlet": KH1ItemData("Accessory", code = 264_1044, classification = ItemClassification.useful, ), + "Heartguard": KH1ItemData("Accessory", code = 264_1045, classification = ItemClassification.useful, ), + "Ribbon": KH1ItemData("Accessory", code = 264_1046, classification = ItemClassification.useful, ), + "Crystal Crown": KH1ItemData("Accessory", code = 264_1047, classification = ItemClassification.useful, ), + "Brave Warrior": KH1ItemData("Accessory", code = 264_1048, classification = ItemClassification.useful, ), + "Ifrit's Horn": KH1ItemData("Accessory", code = 264_1049, classification = ItemClassification.useful, ), + "Inferno Band": KH1ItemData("Accessory", code = 264_1050, classification = ItemClassification.useful, ), + "White Fang": KH1ItemData("Accessory", code = 264_1051, classification = ItemClassification.useful, ), + "Ray of Light": KH1ItemData("Accessory", code = 264_1052, classification = ItemClassification.useful, ), + "Holy Circlet": KH1ItemData("Accessory", code = 264_1053, classification = ItemClassification.useful, ), + "Raven's Claw": KH1ItemData("Accessory", code = 264_1054, classification = ItemClassification.useful, ), + "Omega Arts": KH1ItemData("Accessory", code = 264_1055, classification = ItemClassification.useful, ), + "EXP Earring": KH1ItemData("Accessory", code = 264_1056, classification = ItemClassification.useful, ), + #"A41": KH1ItemData("Accessory", code = 264_1057, classification = ItemClassification.useful, ), + "EXP Ring": KH1ItemData("Accessory", code = 264_1058, classification = ItemClassification.useful, ), + "EXP Bracelet": KH1ItemData("Accessory", code = 264_1059, classification = ItemClassification.useful, ), + "EXP Necklace": KH1ItemData("Accessory", code = 264_1060, classification = ItemClassification.useful, ), + "Firagun Band": KH1ItemData("Accessory", code = 264_1061, classification = ItemClassification.useful, ), + "Blizzagun Band": KH1ItemData("Accessory", code = 264_1062, classification = ItemClassification.useful, ), + "Thundagun Band": KH1ItemData("Accessory", code = 264_1063, classification = ItemClassification.useful, ), + "Ifrit Belt": KH1ItemData("Accessory", code = 264_1064, classification = ItemClassification.useful, ), + "Shiva Belt": KH1ItemData("Accessory", code = 264_1065, classification = ItemClassification.useful, ), + "Ramuh Belt": KH1ItemData("Accessory", code = 264_1066, classification = ItemClassification.useful, ), + "Moogle Badge": KH1ItemData("Accessory", code = 264_1067, classification = ItemClassification.useful, ), + "Cosmic Arts": KH1ItemData("Accessory", code = 264_1068, classification = ItemClassification.useful, ), + "Royal Crown": KH1ItemData("Accessory", code = 264_1069, classification = ItemClassification.useful, ), + "Prime Cap": KH1ItemData("Accessory", code = 264_1070, classification = ItemClassification.useful, ), + "Obsidian Ring": KH1ItemData("Accessory", code = 264_1071, classification = ItemClassification.useful, ), + #"A56": KH1ItemData("Accessory", code = 264_1072, classification = ItemClassification.filler, ), + #"A57": KH1ItemData("Accessory", code = 264_1073, classification = ItemClassification.filler, ), + #"A58": KH1ItemData("Accessory", code = 264_1074, classification = ItemClassification.filler, ), + #"A59": KH1ItemData("Accessory", code = 264_1075, classification = ItemClassification.filler, ), + #"A60": KH1ItemData("Accessory", code = 264_1076, classification = ItemClassification.filler, ), + #"A61": KH1ItemData("Accessory", code = 264_1077, classification = ItemClassification.filler, ), + #"A62": KH1ItemData("Accessory", code = 264_1078, classification = ItemClassification.filler, ), + #"A63": KH1ItemData("Accessory", code = 264_1079, classification = ItemClassification.filler, ), + #"A64": KH1ItemData("Accessory", code = 264_1080, classification = ItemClassification.filler, ), + #"Kingdom Key": KH1ItemData("Keyblades", code = 264_1081, classification = ItemClassification.useful, ), + #"Dream Sword": KH1ItemData("Keyblades", code = 264_1082, classification = ItemClassification.useful, ), + #"Dream Shield": KH1ItemData("Keyblades", code = 264_1083, classification = ItemClassification.useful, ), + #"Dream Rod": KH1ItemData("Keyblades", code = 264_1084, classification = ItemClassification.useful, ), + "Wooden Sword": KH1ItemData("Keyblades", code = 264_1085, classification = ItemClassification.useful, ), + "Jungle King": KH1ItemData("Keyblades", code = 264_1086, classification = ItemClassification.progression, ), + "Three Wishes": KH1ItemData("Keyblades", code = 264_1087, classification = ItemClassification.progression, ), + "Fairy Harp": KH1ItemData("Keyblades", code = 264_1088, classification = ItemClassification.progression, ), + "Pumpkinhead": KH1ItemData("Keyblades", code = 264_1089, classification = ItemClassification.progression, ), + "Crabclaw": KH1ItemData("Keyblades", code = 264_1090, classification = ItemClassification.useful, ), + "Divine Rose": KH1ItemData("Keyblades", code = 264_1091, classification = ItemClassification.progression, ), + "Spellbinder": KH1ItemData("Keyblades", code = 264_1092, classification = ItemClassification.useful, ), + "Olympia": KH1ItemData("Keyblades", code = 264_1093, classification = ItemClassification.progression, ), + "Lionheart": KH1ItemData("Keyblades", code = 264_1094, classification = ItemClassification.progression, ), + "Metal Chocobo": KH1ItemData("Keyblades", code = 264_1095, classification = ItemClassification.useful, ), + "Oathkeeper": KH1ItemData("Keyblades", code = 264_1096, classification = ItemClassification.progression, ), + "Oblivion": KH1ItemData("Keyblades", code = 264_1097, classification = ItemClassification.progression, ), + "Lady Luck": KH1ItemData("Keyblades", code = 264_1098, classification = ItemClassification.progression, ), + "Wishing Star": KH1ItemData("Keyblades", code = 264_1099, classification = ItemClassification.progression, ), + "Ultima Weapon": KH1ItemData("Keyblades", code = 264_1100, classification = ItemClassification.useful, ), + "Diamond Dust": KH1ItemData("Keyblades", code = 264_1101, classification = ItemClassification.useful, ), + "One-Winged Angel": KH1ItemData("Keyblades", code = 264_1102, classification = ItemClassification.useful, ), + #"Mage's Staff": KH1ItemData("Weapons", code = 264_1103, classification = ItemClassification.filler, ), + "Morning Star": KH1ItemData("Weapons", code = 264_1104, classification = ItemClassification.useful, ), + "Shooting Star": KH1ItemData("Weapons", code = 264_1105, classification = ItemClassification.useful, ), + "Magus Staff": KH1ItemData("Weapons", code = 264_1106, classification = ItemClassification.useful, ), + "Wisdom Staff": KH1ItemData("Weapons", code = 264_1107, classification = ItemClassification.useful, ), + "Warhammer": KH1ItemData("Weapons", code = 264_1108, classification = ItemClassification.useful, ), + "Silver Mallet": KH1ItemData("Weapons", code = 264_1109, classification = ItemClassification.useful, ), + "Grand Mallet": KH1ItemData("Weapons", code = 264_1110, classification = ItemClassification.useful, ), + "Lord Fortune": KH1ItemData("Weapons", code = 264_1111, classification = ItemClassification.useful, ), + "Violetta": KH1ItemData("Weapons", code = 264_1112, classification = ItemClassification.useful, ), + "Dream Rod (Donald)": KH1ItemData("Weapons", code = 264_1113, classification = ItemClassification.useful, ), + "Save the Queen": KH1ItemData("Weapons", code = 264_1114, classification = ItemClassification.useful, ), + "Wizard's Relic": KH1ItemData("Weapons", code = 264_1115, classification = ItemClassification.useful, ), + "Meteor Strike": KH1ItemData("Weapons", code = 264_1116, classification = ItemClassification.useful, ), + "Fantasista": KH1ItemData("Weapons", code = 264_1117, classification = ItemClassification.useful, ), + #"Unused (Donald)": KH1ItemData("Weapons", code = 264_1118, classification = ItemClassification.filler, ), + #"Knight's Shield": KH1ItemData("Weapons", code = 264_1119, classification = ItemClassification.filler, ), + "Mythril Shield": KH1ItemData("Weapons", code = 264_1120, classification = ItemClassification.useful, ), + "Onyx Shield": KH1ItemData("Weapons", code = 264_1121, classification = ItemClassification.useful, ), + "Stout Shield": KH1ItemData("Weapons", code = 264_1122, classification = ItemClassification.useful, ), + "Golem Shield": KH1ItemData("Weapons", code = 264_1123, classification = ItemClassification.useful, ), + "Adamant Shield": KH1ItemData("Weapons", code = 264_1124, classification = ItemClassification.useful, ), + "Smasher": KH1ItemData("Weapons", code = 264_1125, classification = ItemClassification.useful, ), + "Gigas Fist": KH1ItemData("Weapons", code = 264_1126, classification = ItemClassification.useful, ), + "Genji Shield": KH1ItemData("Weapons", code = 264_1127, classification = ItemClassification.useful, ), + "Herc's Shield": KH1ItemData("Weapons", code = 264_1128, classification = ItemClassification.useful, ), + "Dream Shield (Goofy)": KH1ItemData("Weapons", code = 264_1129, classification = ItemClassification.useful, ), + "Save the King": KH1ItemData("Weapons", code = 264_1130, classification = ItemClassification.useful, ), + "Defender": KH1ItemData("Weapons", code = 264_1131, classification = ItemClassification.useful, ), + "Mighty Shield": KH1ItemData("Weapons", code = 264_1132, classification = ItemClassification.useful, ), + "Seven Elements": KH1ItemData("Weapons", code = 264_1133, classification = ItemClassification.useful, ), + #"Unused (Goofy)": KH1ItemData("Weapons", code = 264_1134, classification = ItemClassification.filler, ), + #"Spear": KH1ItemData("Weapons", code = 264_1135, classification = ItemClassification.filler, ), + #"No Weapon": KH1ItemData("Weapons", code = 264_1136, classification = ItemClassification.filler, ), + #"Genie": KH1ItemData("Weapons", code = 264_1137, classification = ItemClassification.filler, ), + #"No Weapon": KH1ItemData("Weapons", code = 264_1138, classification = ItemClassification.filler, ), + #"No Weapon": KH1ItemData("Weapons", code = 264_1139, classification = ItemClassification.filler, ), + #"Tinker Bell": KH1ItemData("Weapons", code = 264_1140, classification = ItemClassification.filler, ), + #"Claws": KH1ItemData("Weapons", code = 264_1141, classification = ItemClassification.filler, ), + "Tent": KH1ItemData("Camping", code = 264_1142, classification = ItemClassification.filler, ), + "Camping Set": KH1ItemData("Camping", code = 264_1143, classification = ItemClassification.filler, ), + "Cottage": KH1ItemData("Camping", code = 264_1144, classification = ItemClassification.filler, ), + #"C04": KH1ItemData("Camping", code = 264_1145, classification = ItemClassification.filler, ), + #"C05": KH1ItemData("Camping", code = 264_1146, classification = ItemClassification.filler, ), + #"C06": KH1ItemData("Camping", code = 264_1147, classification = ItemClassification.filler, ), + #"C07": KH1ItemData("Camping", code = 264_1148, classification = ItemClassification.filler, ), + "Ansem's Report 11": KH1ItemData("Reports", code = 264_1149, classification = ItemClassification.progression, ), + "Ansem's Report 12": KH1ItemData("Reports", code = 264_1150, classification = ItemClassification.progression, ), + "Ansem's Report 13": KH1ItemData("Reports", code = 264_1151, classification = ItemClassification.progression, ), + "Power Up": KH1ItemData("Stat Ups", code = 264_1152, classification = ItemClassification.filler, ), + "Defense Up": KH1ItemData("Stat Ups", code = 264_1153, classification = ItemClassification.filler, ), + "AP Up": KH1ItemData("Stat Ups", code = 264_1154, classification = ItemClassification.filler, ), + #"Serenity Power": KH1ItemData("Synthesis", code = 264_1155, classification = ItemClassification.filler, ), + #"Dark Matter": KH1ItemData("Synthesis", code = 264_1156, classification = ItemClassification.filler, ), + #"Mythril Stone": KH1ItemData("Synthesis", code = 264_1157, classification = ItemClassification.filler, ), + "Fire Arts": KH1ItemData("Key", code = 264_1158, classification = ItemClassification.progression, ), + "Blizzard Arts": KH1ItemData("Key", code = 264_1159, classification = ItemClassification.progression, ), + "Thunder Arts": KH1ItemData("Key", code = 264_1160, classification = ItemClassification.progression, ), + "Cure Arts": KH1ItemData("Key", code = 264_1161, classification = ItemClassification.progression, ), + "Gravity Arts": KH1ItemData("Key", code = 264_1162, classification = ItemClassification.progression, ), + "Stop Arts": KH1ItemData("Key", code = 264_1163, classification = ItemClassification.progression, ), + "Aero Arts": KH1ItemData("Key", code = 264_1164, classification = ItemClassification.progression, ), + #"Shiitank Rank": KH1ItemData("Synthesis", code = 264_1165, classification = ItemClassification.filler, ), + #"Matsutake Rank": KH1ItemData("Synthesis", code = 264_1166, classification = ItemClassification.filler, ), + #"Mystery Mold": KH1ItemData("Synthesis", code = 264_1167, classification = ItemClassification.filler, ), + "Ansem's Report 1": KH1ItemData("Reports", code = 264_1168, classification = ItemClassification.progression, ), + "Ansem's Report 2": KH1ItemData("Reports", code = 264_1169, classification = ItemClassification.progression, ), + "Ansem's Report 3": KH1ItemData("Reports", code = 264_1170, classification = ItemClassification.progression, ), + "Ansem's Report 4": KH1ItemData("Reports", code = 264_1171, classification = ItemClassification.progression, ), + "Ansem's Report 5": KH1ItemData("Reports", code = 264_1172, classification = ItemClassification.progression, ), + "Ansem's Report 6": KH1ItemData("Reports", code = 264_1173, classification = ItemClassification.progression, ), + "Ansem's Report 7": KH1ItemData("Reports", code = 264_1174, classification = ItemClassification.progression, ), + "Ansem's Report 8": KH1ItemData("Reports", code = 264_1175, classification = ItemClassification.progression, ), + "Ansem's Report 9": KH1ItemData("Reports", code = 264_1176, classification = ItemClassification.progression, ), + "Ansem's Report 10": KH1ItemData("Reports", code = 264_1177, classification = ItemClassification.progression, ), + #"Khama Vol. 8": KH1ItemData("Key", code = 264_1178, classification = ItemClassification.progression, ), + #"Salegg Vol. 6": KH1ItemData("Key", code = 264_1179, classification = ItemClassification.progression, ), + #"Azal Vol. 3": KH1ItemData("Key", code = 264_1180, classification = ItemClassification.progression, ), + #"Mava Vol. 3": KH1ItemData("Key", code = 264_1181, classification = ItemClassification.progression, ), + #"Mava Vol. 6": KH1ItemData("Key", code = 264_1182, classification = ItemClassification.progression, ), + "Theon Vol. 6": KH1ItemData("Key", code = 264_1183, classification = ItemClassification.progression, ), + #"Nahara Vol. 5": KH1ItemData("Key", code = 264_1184, classification = ItemClassification.progression, ), + #"Hafet Vol. 4": KH1ItemData("Key", code = 264_1185, classification = ItemClassification.progression, ), + "Empty Bottle": KH1ItemData("Key", code = 264_1186, classification = ItemClassification.progression, max_quantity = 6 ), + #"Old Book": KH1ItemData("Key", code = 264_1187, classification = ItemClassification.progression, ), + "Emblem Piece (Flame)": KH1ItemData("Key", code = 264_1188, classification = ItemClassification.progression, ), + "Emblem Piece (Chest)": KH1ItemData("Key", code = 264_1189, classification = ItemClassification.progression, ), + "Emblem Piece (Statue)": KH1ItemData("Key", code = 264_1190, classification = ItemClassification.progression, ), + "Emblem Piece (Fountain)": KH1ItemData("Key", code = 264_1191, classification = ItemClassification.progression, ), + #"Log": KH1ItemData("Key", code = 264_1192, classification = ItemClassification.progression, ), + #"Cloth": KH1ItemData("Key", code = 264_1193, classification = ItemClassification.progression, ), + #"Rope": KH1ItemData("Key", code = 264_1194, classification = ItemClassification.progression, ), + #"Seagull Egg": KH1ItemData("Key", code = 264_1195, classification = ItemClassification.progression, ), + #"Fish": KH1ItemData("Key", code = 264_1196, classification = ItemClassification.progression, ), + #"Mushroom": KH1ItemData("Key", code = 264_1197, classification = ItemClassification.progression, ), + #"Coconut": KH1ItemData("Key", code = 264_1198, classification = ItemClassification.progression, ), + #"Drinking Water": KH1ItemData("Key", code = 264_1199, classification = ItemClassification.progression, ), + #"Navi-G Piece 1": KH1ItemData("Key", code = 264_1200, classification = ItemClassification.progression, ), + #"Navi-G Piece 2": KH1ItemData("Key", code = 264_1201, classification = ItemClassification.progression, ), + #"Navi-Gummi Unused": KH1ItemData("Key", code = 264_1202, classification = ItemClassification.progression, ), + #"Navi-G Piece 3": KH1ItemData("Key", code = 264_1203, classification = ItemClassification.progression, ), + #"Navi-G Piece 4": KH1ItemData("Key", code = 264_1204, classification = ItemClassification.progression, ), + #"Navi-Gummi": KH1ItemData("Key", code = 264_1205, classification = ItemClassification.progression, ), + #"Watergleam": KH1ItemData("Key", code = 264_1206, classification = ItemClassification.progression, ), + #"Naturespark": KH1ItemData("Key", code = 264_1207, classification = ItemClassification.progression, ), + #"Fireglow": KH1ItemData("Key", code = 264_1208, classification = ItemClassification.progression, ), + #"Earthshine": KH1ItemData("Key", code = 264_1209, classification = ItemClassification.progression, ), + "Crystal Trident": KH1ItemData("Key", code = 264_1210, classification = ItemClassification.progression, ), + "Postcard": KH1ItemData("Key", code = 264_1211, classification = ItemClassification.progression, max_quantity = 10), + "Torn Page 1": KH1ItemData("Torn Pages", code = 264_1212, classification = ItemClassification.progression, ), + "Torn Page 2": KH1ItemData("Torn Pages", code = 264_1213, classification = ItemClassification.progression, ), + "Torn Page 3": KH1ItemData("Torn Pages", code = 264_1214, classification = ItemClassification.progression, ), + "Torn Page 4": KH1ItemData("Torn Pages", code = 264_1215, classification = ItemClassification.progression, ), + "Torn Page 5": KH1ItemData("Torn Pages", code = 264_1216, classification = ItemClassification.progression, ), + "Slides": KH1ItemData("Key", code = 264_1217, classification = ItemClassification.progression, ), + #"Slide 2": KH1ItemData("Key", code = 264_1218, classification = ItemClassification.progression, ), + #"Slide 3": KH1ItemData("Key", code = 264_1219, classification = ItemClassification.progression, ), + #"Slide 4": KH1ItemData("Key", code = 264_1220, classification = ItemClassification.progression, ), + #"Slide 5": KH1ItemData("Key", code = 264_1221, classification = ItemClassification.progression, ), + #"Slide 6": KH1ItemData("Key", code = 264_1222, classification = ItemClassification.progression, ), + "Footprints": KH1ItemData("Key", code = 264_1223, classification = ItemClassification.progression, ), + #"Claw Marks": KH1ItemData("Key", code = 264_1224, classification = ItemClassification.progression, ), + #"Stench": KH1ItemData("Key", code = 264_1225, classification = ItemClassification.progression, ), + #"Antenna": KH1ItemData("Key", code = 264_1226, classification = ItemClassification.progression, ), + "Forget-Me-Not": KH1ItemData("Key", code = 264_1227, classification = ItemClassification.progression, ), + "Jack-In-The-Box": KH1ItemData("Key", code = 264_1228, classification = ItemClassification.progression, ), + "Entry Pass": KH1ItemData("Key", code = 264_1229, classification = ItemClassification.progression, ), + #"Hero License": KH1ItemData("Key", code = 264_1230, classification = ItemClassification.progression, ), + #"Pretty Stone": KH1ItemData("Synthesis", code = 264_1231, classification = ItemClassification.filler, ), + #"N41": KH1ItemData("Synthesis", code = 264_1232, classification = ItemClassification.filler, ), + #"Lucid Shard": KH1ItemData("Synthesis", code = 264_1233, classification = ItemClassification.filler, ), + #"Lucid Gem": KH1ItemData("Synthesis", code = 264_1234, classification = ItemClassification.filler, ), + #"Lucid Crystal": KH1ItemData("Synthesis", code = 264_1235, classification = ItemClassification.filler, ), + #"Spirit Shard": KH1ItemData("Synthesis", code = 264_1236, classification = ItemClassification.filler, ), + #"Spirit Gem": KH1ItemData("Synthesis", code = 264_1237, classification = ItemClassification.filler, ), + #"Power Shard": KH1ItemData("Synthesis", code = 264_1238, classification = ItemClassification.filler, ), + #"Power Gem": KH1ItemData("Synthesis", code = 264_1239, classification = ItemClassification.filler, ), + #"Power Crystal": KH1ItemData("Synthesis", code = 264_1240, classification = ItemClassification.filler, ), + #"Blaze Shard": KH1ItemData("Synthesis", code = 264_1241, classification = ItemClassification.filler, ), + #"Blaze Gem": KH1ItemData("Synthesis", code = 264_1242, classification = ItemClassification.filler, ), + #"Frost Shard": KH1ItemData("Synthesis", code = 264_1243, classification = ItemClassification.filler, ), + #"Frost Gem": KH1ItemData("Synthesis", code = 264_1244, classification = ItemClassification.filler, ), + #"Thunder Shard": KH1ItemData("Synthesis", code = 264_1245, classification = ItemClassification.filler, ), + #"Thunder Gem": KH1ItemData("Synthesis", code = 264_1246, classification = ItemClassification.filler, ), + #"Shiny Crystal": KH1ItemData("Synthesis", code = 264_1247, classification = ItemClassification.filler, ), + #"Bright Shard": KH1ItemData("Synthesis", code = 264_1248, classification = ItemClassification.filler, ), + #"Bright Gem": KH1ItemData("Synthesis", code = 264_1249, classification = ItemClassification.filler, ), + #"Bright Crystal": KH1ItemData("Synthesis", code = 264_1250, classification = ItemClassification.filler, ), + #"Mystery Goo": KH1ItemData("Synthesis", code = 264_1251, classification = ItemClassification.filler, ), + #"Gale": KH1ItemData("Synthesis", code = 264_1252, classification = ItemClassification.filler, ), + #"Mythril Shard": KH1ItemData("Synthesis", code = 264_1253, classification = ItemClassification.filler, ), + #"Mythril": KH1ItemData("Synthesis", code = 264_1254, classification = ItemClassification.filler, ), + #"Orichalcum": KH1ItemData("Synthesis", code = 264_1255, classification = ItemClassification.filler, ), + "High Jump": KH1ItemData("Shared Abilities", code = 264_2001, classification = ItemClassification.progression, ), + "Mermaid Kick": KH1ItemData("Shared Abilities", code = 264_2002, classification = ItemClassification.progression, ), + "Progressive Glide": KH1ItemData("Shared Abilities", code = 264_2003, classification = ItemClassification.progression, max_quantity = 2 ), + #"Superglide": KH1ItemData("Shared Abilities", code = 264_2004, classification = ItemClassification.progression, ), + "Puppy 01": KH1ItemData("Puppies", code = 264_2101, classification = ItemClassification.progression, ), + "Puppy 02": KH1ItemData("Puppies", code = 264_2102, classification = ItemClassification.progression, ), + "Puppy 03": KH1ItemData("Puppies", code = 264_2103, classification = ItemClassification.progression, ), + "Puppy 04": KH1ItemData("Puppies", code = 264_2104, classification = ItemClassification.progression, ), + "Puppy 05": KH1ItemData("Puppies", code = 264_2105, classification = ItemClassification.progression, ), + "Puppy 06": KH1ItemData("Puppies", code = 264_2106, classification = ItemClassification.progression, ), + "Puppy 07": KH1ItemData("Puppies", code = 264_2107, classification = ItemClassification.progression, ), + "Puppy 08": KH1ItemData("Puppies", code = 264_2108, classification = ItemClassification.progression, ), + "Puppy 09": KH1ItemData("Puppies", code = 264_2109, classification = ItemClassification.progression, ), + "Puppy 10": KH1ItemData("Puppies", code = 264_2110, classification = ItemClassification.progression, ), + "Puppy 11": KH1ItemData("Puppies", code = 264_2111, classification = ItemClassification.progression, ), + "Puppy 12": KH1ItemData("Puppies", code = 264_2112, classification = ItemClassification.progression, ), + "Puppy 13": KH1ItemData("Puppies", code = 264_2113, classification = ItemClassification.progression, ), + "Puppy 14": KH1ItemData("Puppies", code = 264_2114, classification = ItemClassification.progression, ), + "Puppy 15": KH1ItemData("Puppies", code = 264_2115, classification = ItemClassification.progression, ), + "Puppy 16": KH1ItemData("Puppies", code = 264_2116, classification = ItemClassification.progression, ), + "Puppy 17": KH1ItemData("Puppies", code = 264_2117, classification = ItemClassification.progression, ), + "Puppy 18": KH1ItemData("Puppies", code = 264_2118, classification = ItemClassification.progression, ), + "Puppy 19": KH1ItemData("Puppies", code = 264_2119, classification = ItemClassification.progression, ), + "Puppy 20": KH1ItemData("Puppies", code = 264_2120, classification = ItemClassification.progression, ), + "Puppy 21": KH1ItemData("Puppies", code = 264_2121, classification = ItemClassification.progression, ), + "Puppy 22": KH1ItemData("Puppies", code = 264_2122, classification = ItemClassification.progression, ), + "Puppy 23": KH1ItemData("Puppies", code = 264_2123, classification = ItemClassification.progression, ), + "Puppy 24": KH1ItemData("Puppies", code = 264_2124, classification = ItemClassification.progression, ), + "Puppy 25": KH1ItemData("Puppies", code = 264_2125, classification = ItemClassification.progression, ), + "Puppy 26": KH1ItemData("Puppies", code = 264_2126, classification = ItemClassification.progression, ), + "Puppy 27": KH1ItemData("Puppies", code = 264_2127, classification = ItemClassification.progression, ), + "Puppy 28": KH1ItemData("Puppies", code = 264_2128, classification = ItemClassification.progression, ), + "Puppy 29": KH1ItemData("Puppies", code = 264_2129, classification = ItemClassification.progression, ), + "Puppy 30": KH1ItemData("Puppies", code = 264_2130, classification = ItemClassification.progression, ), + "Puppy 31": KH1ItemData("Puppies", code = 264_2131, classification = ItemClassification.progression, ), + "Puppy 32": KH1ItemData("Puppies", code = 264_2132, classification = ItemClassification.progression, ), + "Puppy 33": KH1ItemData("Puppies", code = 264_2133, classification = ItemClassification.progression, ), + "Puppy 34": KH1ItemData("Puppies", code = 264_2134, classification = ItemClassification.progression, ), + "Puppy 35": KH1ItemData("Puppies", code = 264_2135, classification = ItemClassification.progression, ), + "Puppy 36": KH1ItemData("Puppies", code = 264_2136, classification = ItemClassification.progression, ), + "Puppy 37": KH1ItemData("Puppies", code = 264_2137, classification = ItemClassification.progression, ), + "Puppy 38": KH1ItemData("Puppies", code = 264_2138, classification = ItemClassification.progression, ), + "Puppy 39": KH1ItemData("Puppies", code = 264_2139, classification = ItemClassification.progression, ), + "Puppy 40": KH1ItemData("Puppies", code = 264_2140, classification = ItemClassification.progression, ), + "Puppy 41": KH1ItemData("Puppies", code = 264_2141, classification = ItemClassification.progression, ), + "Puppy 42": KH1ItemData("Puppies", code = 264_2142, classification = ItemClassification.progression, ), + "Puppy 43": KH1ItemData("Puppies", code = 264_2143, classification = ItemClassification.progression, ), + "Puppy 44": KH1ItemData("Puppies", code = 264_2144, classification = ItemClassification.progression, ), + "Puppy 45": KH1ItemData("Puppies", code = 264_2145, classification = ItemClassification.progression, ), + "Puppy 46": KH1ItemData("Puppies", code = 264_2146, classification = ItemClassification.progression, ), + "Puppy 47": KH1ItemData("Puppies", code = 264_2147, classification = ItemClassification.progression, ), + "Puppy 48": KH1ItemData("Puppies", code = 264_2148, classification = ItemClassification.progression, ), + "Puppy 49": KH1ItemData("Puppies", code = 264_2149, classification = ItemClassification.progression, ), + "Puppy 50": KH1ItemData("Puppies", code = 264_2150, classification = ItemClassification.progression, ), + "Puppy 51": KH1ItemData("Puppies", code = 264_2151, classification = ItemClassification.progression, ), + "Puppy 52": KH1ItemData("Puppies", code = 264_2152, classification = ItemClassification.progression, ), + "Puppy 53": KH1ItemData("Puppies", code = 264_2153, classification = ItemClassification.progression, ), + "Puppy 54": KH1ItemData("Puppies", code = 264_2154, classification = ItemClassification.progression, ), + "Puppy 55": KH1ItemData("Puppies", code = 264_2155, classification = ItemClassification.progression, ), + "Puppy 56": KH1ItemData("Puppies", code = 264_2156, classification = ItemClassification.progression, ), + "Puppy 57": KH1ItemData("Puppies", code = 264_2157, classification = ItemClassification.progression, ), + "Puppy 58": KH1ItemData("Puppies", code = 264_2158, classification = ItemClassification.progression, ), + "Puppy 59": KH1ItemData("Puppies", code = 264_2159, classification = ItemClassification.progression, ), + "Puppy 60": KH1ItemData("Puppies", code = 264_2160, classification = ItemClassification.progression, ), + "Puppy 61": KH1ItemData("Puppies", code = 264_2161, classification = ItemClassification.progression, ), + "Puppy 62": KH1ItemData("Puppies", code = 264_2162, classification = ItemClassification.progression, ), + "Puppy 63": KH1ItemData("Puppies", code = 264_2163, classification = ItemClassification.progression, ), + "Puppy 64": KH1ItemData("Puppies", code = 264_2164, classification = ItemClassification.progression, ), + "Puppy 65": KH1ItemData("Puppies", code = 264_2165, classification = ItemClassification.progression, ), + "Puppy 66": KH1ItemData("Puppies", code = 264_2166, classification = ItemClassification.progression, ), + "Puppy 67": KH1ItemData("Puppies", code = 264_2167, classification = ItemClassification.progression, ), + "Puppy 68": KH1ItemData("Puppies", code = 264_2168, classification = ItemClassification.progression, ), + "Puppy 69": KH1ItemData("Puppies", code = 264_2169, classification = ItemClassification.progression, ), + "Puppy 70": KH1ItemData("Puppies", code = 264_2170, classification = ItemClassification.progression, ), + "Puppy 71": KH1ItemData("Puppies", code = 264_2171, classification = ItemClassification.progression, ), + "Puppy 72": KH1ItemData("Puppies", code = 264_2172, classification = ItemClassification.progression, ), + "Puppy 73": KH1ItemData("Puppies", code = 264_2173, classification = ItemClassification.progression, ), + "Puppy 74": KH1ItemData("Puppies", code = 264_2174, classification = ItemClassification.progression, ), + "Puppy 75": KH1ItemData("Puppies", code = 264_2175, classification = ItemClassification.progression, ), + "Puppy 76": KH1ItemData("Puppies", code = 264_2176, classification = ItemClassification.progression, ), + "Puppy 77": KH1ItemData("Puppies", code = 264_2177, classification = ItemClassification.progression, ), + "Puppy 78": KH1ItemData("Puppies", code = 264_2178, classification = ItemClassification.progression, ), + "Puppy 79": KH1ItemData("Puppies", code = 264_2179, classification = ItemClassification.progression, ), + "Puppy 80": KH1ItemData("Puppies", code = 264_2180, classification = ItemClassification.progression, ), + "Puppy 81": KH1ItemData("Puppies", code = 264_2181, classification = ItemClassification.progression, ), + "Puppy 82": KH1ItemData("Puppies", code = 264_2182, classification = ItemClassification.progression, ), + "Puppy 83": KH1ItemData("Puppies", code = 264_2183, classification = ItemClassification.progression, ), + "Puppy 84": KH1ItemData("Puppies", code = 264_2184, classification = ItemClassification.progression, ), + "Puppy 85": KH1ItemData("Puppies", code = 264_2185, classification = ItemClassification.progression, ), + "Puppy 86": KH1ItemData("Puppies", code = 264_2186, classification = ItemClassification.progression, ), + "Puppy 87": KH1ItemData("Puppies", code = 264_2187, classification = ItemClassification.progression, ), + "Puppy 88": KH1ItemData("Puppies", code = 264_2188, classification = ItemClassification.progression, ), + "Puppy 89": KH1ItemData("Puppies", code = 264_2189, classification = ItemClassification.progression, ), + "Puppy 90": KH1ItemData("Puppies", code = 264_2190, classification = ItemClassification.progression, ), + "Puppy 91": KH1ItemData("Puppies", code = 264_2191, classification = ItemClassification.progression, ), + "Puppy 92": KH1ItemData("Puppies", code = 264_2192, classification = ItemClassification.progression, ), + "Puppy 93": KH1ItemData("Puppies", code = 264_2193, classification = ItemClassification.progression, ), + "Puppy 94": KH1ItemData("Puppies", code = 264_2194, classification = ItemClassification.progression, ), + "Puppy 95": KH1ItemData("Puppies", code = 264_2195, classification = ItemClassification.progression, ), + "Puppy 96": KH1ItemData("Puppies", code = 264_2196, classification = ItemClassification.progression, ), + "Puppy 97": KH1ItemData("Puppies", code = 264_2197, classification = ItemClassification.progression, ), + "Puppy 98": KH1ItemData("Puppies", code = 264_2198, classification = ItemClassification.progression, ), + "Puppy 99": KH1ItemData("Puppies", code = 264_2199, classification = ItemClassification.progression, ), + "Puppies 01-03": KH1ItemData("Puppies", code = 264_2201, classification = ItemClassification.progression, ), + "Puppies 04-06": KH1ItemData("Puppies", code = 264_2202, classification = ItemClassification.progression, ), + "Puppies 07-09": KH1ItemData("Puppies", code = 264_2203, classification = ItemClassification.progression, ), + "Puppies 10-12": KH1ItemData("Puppies", code = 264_2204, classification = ItemClassification.progression, ), + "Puppies 13-15": KH1ItemData("Puppies", code = 264_2205, classification = ItemClassification.progression, ), + "Puppies 16-18": KH1ItemData("Puppies", code = 264_2206, classification = ItemClassification.progression, ), + "Puppies 19-21": KH1ItemData("Puppies", code = 264_2207, classification = ItemClassification.progression, ), + "Puppies 22-24": KH1ItemData("Puppies", code = 264_2208, classification = ItemClassification.progression, ), + "Puppies 25-27": KH1ItemData("Puppies", code = 264_2209, classification = ItemClassification.progression, ), + "Puppies 28-30": KH1ItemData("Puppies", code = 264_2210, classification = ItemClassification.progression, ), + "Puppies 31-33": KH1ItemData("Puppies", code = 264_2211, classification = ItemClassification.progression, ), + "Puppies 34-36": KH1ItemData("Puppies", code = 264_2212, classification = ItemClassification.progression, ), + "Puppies 37-39": KH1ItemData("Puppies", code = 264_2213, classification = ItemClassification.progression, ), + "Puppies 40-42": KH1ItemData("Puppies", code = 264_2214, classification = ItemClassification.progression, ), + "Puppies 43-45": KH1ItemData("Puppies", code = 264_2215, classification = ItemClassification.progression, ), + "Puppies 46-48": KH1ItemData("Puppies", code = 264_2216, classification = ItemClassification.progression, ), + "Puppies 49-51": KH1ItemData("Puppies", code = 264_2217, classification = ItemClassification.progression, ), + "Puppies 52-54": KH1ItemData("Puppies", code = 264_2218, classification = ItemClassification.progression, ), + "Puppies 55-57": KH1ItemData("Puppies", code = 264_2219, classification = ItemClassification.progression, ), + "Puppies 58-60": KH1ItemData("Puppies", code = 264_2220, classification = ItemClassification.progression, ), + "Puppies 61-63": KH1ItemData("Puppies", code = 264_2221, classification = ItemClassification.progression, ), + "Puppies 64-66": KH1ItemData("Puppies", code = 264_2222, classification = ItemClassification.progression, ), + "Puppies 67-69": KH1ItemData("Puppies", code = 264_2223, classification = ItemClassification.progression, ), + "Puppies 70-72": KH1ItemData("Puppies", code = 264_2224, classification = ItemClassification.progression, ), + "Puppies 73-75": KH1ItemData("Puppies", code = 264_2225, classification = ItemClassification.progression, ), + "Puppies 76-78": KH1ItemData("Puppies", code = 264_2226, classification = ItemClassification.progression, ), + "Puppies 79-81": KH1ItemData("Puppies", code = 264_2227, classification = ItemClassification.progression, ), + "Puppies 82-84": KH1ItemData("Puppies", code = 264_2228, classification = ItemClassification.progression, ), + "Puppies 85-87": KH1ItemData("Puppies", code = 264_2229, classification = ItemClassification.progression, ), + "Puppies 88-90": KH1ItemData("Puppies", code = 264_2230, classification = ItemClassification.progression, ), + "Puppies 91-93": KH1ItemData("Puppies", code = 264_2231, classification = ItemClassification.progression, ), + "Puppies 94-96": KH1ItemData("Puppies", code = 264_2232, classification = ItemClassification.progression, ), + "Puppies 97-99": KH1ItemData("Puppies", code = 264_2233, classification = ItemClassification.progression, ), + "All Puppies": KH1ItemData("Puppies", code = 264_2240, classification = ItemClassification.progression, ), + "Treasure Magnet": KH1ItemData("Abilities", code = 264_3005, classification = ItemClassification.useful, max_quantity = 2 ), + "Combo Plus": KH1ItemData("Abilities", code = 264_3006, classification = ItemClassification.useful, max_quantity = 4 ), + "Air Combo Plus": KH1ItemData("Abilities", code = 264_3007, classification = ItemClassification.useful, max_quantity = 2 ), + "Critical Plus": KH1ItemData("Abilities", code = 264_3008, classification = ItemClassification.useful, max_quantity = 3 ), + #"Second Wind": KH1ItemData("Abilities", code = 264_3009, classification = ItemClassification.useful, ), + "Scan": KH1ItemData("Abilities", code = 264_3010, classification = ItemClassification.useful, ), + "Sonic Blade": KH1ItemData("Abilities", code = 264_3011, classification = ItemClassification.useful, ), + "Ars Arcanum": KH1ItemData("Abilities", code = 264_3012, classification = ItemClassification.useful, ), + "Strike Raid": KH1ItemData("Abilities", code = 264_3013, classification = ItemClassification.useful, ), + "Ragnarok": KH1ItemData("Abilities", code = 264_3014, classification = ItemClassification.useful, ), + "Trinity Limit": KH1ItemData("Abilities", code = 264_3015, classification = ItemClassification.useful, ), + "Cheer": KH1ItemData("Abilities", code = 264_3016, classification = ItemClassification.useful, ), + "Vortex": KH1ItemData("Abilities", code = 264_3017, classification = ItemClassification.useful, ), + "Aerial Sweep": KH1ItemData("Abilities", code = 264_3018, classification = ItemClassification.useful, ), + "Counterattack": KH1ItemData("Abilities", code = 264_3019, classification = ItemClassification.useful, ), + "Blitz": KH1ItemData("Abilities", code = 264_3020, classification = ItemClassification.useful, ), + "Guard": KH1ItemData("Abilities", code = 264_3021, classification = ItemClassification.progression, ), + "Dodge Roll": KH1ItemData("Abilities", code = 264_3022, classification = ItemClassification.progression, ), + "MP Haste": KH1ItemData("Abilities", code = 264_3023, classification = ItemClassification.useful, ), + "MP Rage": KH1ItemData("Abilities", code = 264_3024, classification = ItemClassification.progression, ), + "Second Chance": KH1ItemData("Abilities", code = 264_3025, classification = ItemClassification.progression, ), + "Berserk": KH1ItemData("Abilities", code = 264_3026, classification = ItemClassification.useful, ), + "Jackpot": KH1ItemData("Abilities", code = 264_3027, classification = ItemClassification.useful, ), + "Lucky Strike": KH1ItemData("Abilities", code = 264_3028, classification = ItemClassification.useful, ), + #"Charge": KH1ItemData("Abilities", code = 264_3029, classification = ItemClassification.useful, ), + #"Rocket": KH1ItemData("Abilities", code = 264_3030, classification = ItemClassification.useful, ), + #"Tornado": KH1ItemData("Abilities", code = 264_3031, classification = ItemClassification.useful, ), + #"MP Gift": KH1ItemData("Abilities", code = 264_3032, classification = ItemClassification.useful, ), + #"Raging Boar": KH1ItemData("Abilities", code = 264_3033, classification = ItemClassification.useful, ), + #"Asp's Bite": KH1ItemData("Abilities", code = 264_3034, classification = ItemClassification.useful, ), + #"Healing Herb": KH1ItemData("Abilities", code = 264_3035, classification = ItemClassification.useful, ), + #"Wind Armor": KH1ItemData("Abilities", code = 264_3036, classification = ItemClassification.useful, ), + #"Crescent": KH1ItemData("Abilities", code = 264_3037, classification = ItemClassification.useful, ), + #"Sandstorm": KH1ItemData("Abilities", code = 264_3038, classification = ItemClassification.useful, ), + #"Applause!": KH1ItemData("Abilities", code = 264_3039, classification = ItemClassification.useful, ), + #"Blazing Fury": KH1ItemData("Abilities", code = 264_3040, classification = ItemClassification.useful, ), + #"Icy Terror": KH1ItemData("Abilities", code = 264_3041, classification = ItemClassification.useful, ), + #"Bolts of Sorrow": KH1ItemData("Abilities", code = 264_3042, classification = ItemClassification.useful, ), + #"Ghostly Scream": KH1ItemData("Abilities", code = 264_3043, classification = ItemClassification.useful, ), + #"Humming Bird": KH1ItemData("Abilities", code = 264_3044, classification = ItemClassification.useful, ), + #"Time-Out": KH1ItemData("Abilities", code = 264_3045, classification = ItemClassification.useful, ), + #"Storm's Eye": KH1ItemData("Abilities", code = 264_3046, classification = ItemClassification.useful, ), + #"Ferocious Lunge": KH1ItemData("Abilities", code = 264_3047, classification = ItemClassification.useful, ), + #"Furious Bellow": KH1ItemData("Abilities", code = 264_3048, classification = ItemClassification.useful, ), + #"Spiral Wave": KH1ItemData("Abilities", code = 264_3049, classification = ItemClassification.useful, ), + #"Thunder Potion": KH1ItemData("Abilities", code = 264_3050, classification = ItemClassification.useful, ), + #"Cure Potion": KH1ItemData("Abilities", code = 264_3051, classification = ItemClassification.useful, ), + #"Aero Potion": KH1ItemData("Abilities", code = 264_3052, classification = ItemClassification.useful, ), + "Slapshot": KH1ItemData("Abilities", code = 264_3053, classification = ItemClassification.useful, ), + "Sliding Dash": KH1ItemData("Abilities", code = 264_3054, classification = ItemClassification.useful, ), + "Hurricane Blast": KH1ItemData("Abilities", code = 264_3055, classification = ItemClassification.useful, ), + "Ripple Drive": KH1ItemData("Abilities", code = 264_3056, classification = ItemClassification.useful, ), + "Stun Impact": KH1ItemData("Abilities", code = 264_3057, classification = ItemClassification.useful, ), + "Gravity Break": KH1ItemData("Abilities", code = 264_3058, classification = ItemClassification.useful, ), + "Zantetsuken": KH1ItemData("Abilities", code = 264_3059, classification = ItemClassification.useful, ), + "Tech Boost": KH1ItemData("Abilities", code = 264_3060, classification = ItemClassification.useful, max_quantity = 4 ), + "Encounter Plus": KH1ItemData("Abilities", code = 264_3061, classification = ItemClassification.useful, ), + "Leaf Bracer": KH1ItemData("Abilities", code = 264_3062, classification = ItemClassification.progression, ), + #"Evolution": KH1ItemData("Abilities", code = 264_3063, classification = ItemClassification.useful, ), + "EXP Zero": KH1ItemData("Abilities", code = 264_3064, classification = ItemClassification.useful, ), + "Combo Master": KH1ItemData("Abilities", code = 264_3065, classification = ItemClassification.progression, ), + "Max HP Increase": KH1ItemData("Level Up", code = 264_4001, classification = ItemClassification.useful, max_quantity = 15), + "Max MP Increase": KH1ItemData("Level Up", code = 264_4002, classification = ItemClassification.useful, max_quantity = 15), + "Max AP Increase": KH1ItemData("Level Up", code = 264_4003, classification = ItemClassification.useful, max_quantity = 15), + "Strength Increase": KH1ItemData("Level Up", code = 264_4004, classification = ItemClassification.useful, max_quantity = 15), + "Defense Increase": KH1ItemData("Level Up", code = 264_4005, classification = ItemClassification.useful, max_quantity = 15), + "Accessory Slot Increase": KH1ItemData("Limited Level Up", code = 264_4006, classification = ItemClassification.useful, max_quantity = 15), + "Item Slot Increase": KH1ItemData("Limited Level Up", code = 264_4007, classification = ItemClassification.useful, max_quantity = 15), + "Dumbo": KH1ItemData("Summons", code = 264_5000, classification = ItemClassification.progression, ), + "Bambi": KH1ItemData("Summons", code = 264_5001, classification = ItemClassification.progression, ), + "Genie": KH1ItemData("Summons", code = 264_5002, classification = ItemClassification.progression, ), + "Tinker Bell": KH1ItemData("Summons", code = 264_5003, classification = ItemClassification.progression, ), + "Mushu": KH1ItemData("Summons", code = 264_5004, classification = ItemClassification.progression, ), + "Simba": KH1ItemData("Summons", code = 264_5005, classification = ItemClassification.progression, ), + "Progressive Fire": KH1ItemData("Magic", code = 264_6001, classification = ItemClassification.progression, max_quantity = 3 ), + "Progressive Blizzard": KH1ItemData("Magic", code = 264_6002, classification = ItemClassification.progression, max_quantity = 3 ), + "Progressive Thunder": KH1ItemData("Magic", code = 264_6003, classification = ItemClassification.progression, max_quantity = 3 ), + "Progressive Cure": KH1ItemData("Magic", code = 264_6004, classification = ItemClassification.progression, max_quantity = 3 ), + "Progressive Gravity": KH1ItemData("Magic", code = 264_6005, classification = ItemClassification.progression, max_quantity = 3 ), + "Progressive Stop": KH1ItemData("Magic", code = 264_6006, classification = ItemClassification.progression, max_quantity = 3 ), + "Progressive Aero": KH1ItemData("Magic", code = 264_6007, classification = ItemClassification.progression, max_quantity = 3 ), + #"Traverse Town": KH1ItemData("Worlds", code = 264_7001, classification = ItemClassification.progression, ), + "Wonderland": KH1ItemData("Worlds", code = 264_7002, classification = ItemClassification.progression, ), + "Olympus Coliseum": KH1ItemData("Worlds", code = 264_7003, classification = ItemClassification.progression, ), + "Deep Jungle": KH1ItemData("Worlds", code = 264_7004, classification = ItemClassification.progression, ), + "Agrabah": KH1ItemData("Worlds", code = 264_7005, classification = ItemClassification.progression, ), + "Halloween Town": KH1ItemData("Worlds", code = 264_7006, classification = ItemClassification.progression, ), + "Atlantica": KH1ItemData("Worlds", code = 264_7007, classification = ItemClassification.progression, ), + "Neverland": KH1ItemData("Worlds", code = 264_7008, classification = ItemClassification.progression, ), + "Hollow Bastion": KH1ItemData("Worlds", code = 264_7009, classification = ItemClassification.progression, ), + "End of the World": KH1ItemData("Worlds", code = 264_7010, classification = ItemClassification.progression, ), + "Monstro": KH1ItemData("Worlds", code = 264_7011, classification = ItemClassification.progression, ), + "Blue Trinity": KH1ItemData("Trinities", code = 264_8001, classification = ItemClassification.progression, ), + "Red Trinity": KH1ItemData("Trinities", code = 264_8002, classification = ItemClassification.progression, ), + "Green Trinity": KH1ItemData("Trinities", code = 264_8003, classification = ItemClassification.progression, ), + "Yellow Trinity": KH1ItemData("Trinities", code = 264_8004, classification = ItemClassification.progression, ), + "White Trinity": KH1ItemData("Trinities", code = 264_8005, classification = ItemClassification.progression, ), + "Phil Cup": KH1ItemData("Cups", code = 264_9001, classification = ItemClassification.progression, ), + "Pegasus Cup": KH1ItemData("Cups", code = 264_9002, classification = ItemClassification.progression, ), + "Hercules Cup": KH1ItemData("Cups", code = 264_9003, classification = ItemClassification.progression, ), + #"Hades Cup": KH1ItemData("Cups", code = 264_9004, classification = ItemClassification.progression, ), +} + +event_item_table: Dict[str, KH1ItemData] = {} + +#Make item categories +item_name_groups: Dict[str, Set[str]] = {} +for item in item_table.keys(): + category = item_table[item].category + if category not in item_name_groups.keys(): + item_name_groups[category] = set() + item_name_groups[category].add(item) diff --git a/worlds/kh1/Locations.py b/worlds/kh1/Locations.py new file mode 100644 index 000000000000..a82be70f090b --- /dev/null +++ b/worlds/kh1/Locations.py @@ -0,0 +1,590 @@ +from typing import Dict, NamedTuple, Optional, Set +import typing + + +from BaseClasses import Location + + +class KH1Location(Location): + game: str = "Kingdom Hearts" + + +class KH1LocationData(NamedTuple): + category: str + code: int + + +def get_locations_by_category(category: str) -> Dict[str, KH1LocationData]: + location_dict: Dict[str, KH1LocationData] = {} + for name, data in location_table.items(): + if data.category == category: + location_dict.setdefault(name, data) + + return location_dict + + +location_table: Dict[str, KH1LocationData] = { + #"Destiny Islands Chest": KH1LocationData("Destiny Islands", 265_0011), missable + "Traverse Town 1st District Candle Puzzle Chest": KH1LocationData("Traverse Town", 265_0211), + "Traverse Town 1st District Accessory Shop Roof Chest": KH1LocationData("Traverse Town", 265_0212), + "Traverse Town 2nd District Boots and Shoes Awning Chest": KH1LocationData("Traverse Town", 265_0213), + "Traverse Town 2nd District Rooftop Chest": KH1LocationData("Traverse Town", 265_0214), + "Traverse Town 2nd District Gizmo Shop Facade Chest": KH1LocationData("Traverse Town", 265_0251), + "Traverse Town Alleyway Balcony Chest": KH1LocationData("Traverse Town", 265_0252), + "Traverse Town Alleyway Blue Room Awning Chest": KH1LocationData("Traverse Town", 265_0253), + "Traverse Town Alleyway Corner Chest": KH1LocationData("Traverse Town", 265_0254), + "Traverse Town Green Room Clock Puzzle Chest": KH1LocationData("Traverse Town", 265_0292), + "Traverse Town Green Room Table Chest": KH1LocationData("Traverse Town", 265_0293), + "Traverse Town Red Room Chest": KH1LocationData("Traverse Town", 265_0294), + "Traverse Town Mystical House Yellow Trinity Chest": KH1LocationData("Traverse Town", 265_0331), + "Traverse Town Accessory Shop Chest": KH1LocationData("Traverse Town", 265_0332), + "Traverse Town Secret Waterway White Trinity Chest": KH1LocationData("Traverse Town", 265_0333), + "Traverse Town Geppetto's House Chest": KH1LocationData("Traverse Town", 265_0334), + "Traverse Town Item Workshop Right Chest": KH1LocationData("Traverse Town", 265_0371), + "Traverse Town 1st District Blue Trinity Balcony Chest": KH1LocationData("Traverse Town", 265_0411), + "Traverse Town Mystical House Glide Chest": KH1LocationData("Traverse Town", 265_0891), + "Traverse Town Alleyway Behind Crates Chest": KH1LocationData("Traverse Town", 265_0892), + "Traverse Town Item Workshop Left Chest": KH1LocationData("Traverse Town", 265_0893), + "Traverse Town Secret Waterway Near Stairs Chest": KH1LocationData("Traverse Town", 265_0894), + "Wonderland Rabbit Hole Green Trinity Chest": KH1LocationData("Wonderland", 265_0931), + "Wonderland Rabbit Hole Defeat Heartless 1 Chest": KH1LocationData("Wonderland", 265_0932), + "Wonderland Rabbit Hole Defeat Heartless 2 Chest": KH1LocationData("Wonderland", 265_0933), + "Wonderland Rabbit Hole Defeat Heartless 3 Chest": KH1LocationData("Wonderland", 265_0934), + "Wonderland Bizarre Room Green Trinity Chest": KH1LocationData("Wonderland", 265_0971), + "Wonderland Queen's Castle Hedge Left Red Chest": KH1LocationData("Wonderland", 265_1011), + "Wonderland Queen's Castle Hedge Right Blue Chest": KH1LocationData("Wonderland", 265_1012), + "Wonderland Queen's Castle Hedge Right Red Chest": KH1LocationData("Wonderland", 265_1013), + "Wonderland Lotus Forest Thunder Plant Chest": KH1LocationData("Wonderland", 265_1014), + "Wonderland Lotus Forest Through the Painting Thunder Plant Chest": KH1LocationData("Wonderland", 265_1051), + "Wonderland Lotus Forest Glide Chest": KH1LocationData("Wonderland", 265_1052), + "Wonderland Lotus Forest Nut Chest": KH1LocationData("Wonderland", 265_1053), + "Wonderland Lotus Forest Corner Chest": KH1LocationData("Wonderland", 265_1054), + "Wonderland Bizarre Room Lamp Chest": KH1LocationData("Wonderland", 265_1091), + "Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest": KH1LocationData("Wonderland", 265_1093), + "Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest": KH1LocationData("Wonderland", 265_1094), + "Wonderland Tea Party Garden Bear and Clock Puzzle Chest": KH1LocationData("Wonderland", 265_1131), + "Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest": KH1LocationData("Wonderland", 265_1132), + "Wonderland Lotus Forest Through the Painting White Trinity Chest": KH1LocationData("Wonderland", 265_1133), + "Deep Jungle Tree House Beneath Tree House Chest": KH1LocationData("Deep Jungle", 265_1213), + "Deep Jungle Tree House Rooftop Chest": KH1LocationData("Deep Jungle", 265_1214), + "Deep Jungle Hippo's Lagoon Center Chest": KH1LocationData("Deep Jungle", 265_1251), + "Deep Jungle Hippo's Lagoon Left Chest": KH1LocationData("Deep Jungle", 265_1252), + "Deep Jungle Hippo's Lagoon Right Chest": KH1LocationData("Deep Jungle", 265_1253), + "Deep Jungle Vines Chest": KH1LocationData("Deep Jungle", 265_1291), + "Deep Jungle Vines 2 Chest": KH1LocationData("Deep Jungle", 265_1292), + "Deep Jungle Climbing Trees Blue Trinity Chest": KH1LocationData("Deep Jungle", 265_1293), + "Deep Jungle Tunnel Chest": KH1LocationData("Deep Jungle", 265_1331), + "Deep Jungle Cavern of Hearts White Trinity Chest": KH1LocationData("Deep Jungle", 265_1332), + "Deep Jungle Camp Blue Trinity Chest": KH1LocationData("Deep Jungle", 265_1333), + "Deep Jungle Tent Chest": KH1LocationData("Deep Jungle", 265_1334), + "Deep Jungle Waterfall Cavern Low Chest": KH1LocationData("Deep Jungle", 265_1371), + "Deep Jungle Waterfall Cavern Middle Chest": KH1LocationData("Deep Jungle", 265_1372), + "Deep Jungle Waterfall Cavern High Wall Chest": KH1LocationData("Deep Jungle", 265_1373), + "Deep Jungle Waterfall Cavern High Middle Chest": KH1LocationData("Deep Jungle", 265_1374), + "Deep Jungle Cliff Right Cliff Left Chest": KH1LocationData("Deep Jungle", 265_1411), + "Deep Jungle Cliff Right Cliff Right Chest": KH1LocationData("Deep Jungle", 265_1412), + "Deep Jungle Tree House Suspended Boat Chest": KH1LocationData("Deep Jungle", 265_1413), + "100 Acre Wood Meadow Inside Log Chest": KH1LocationData("100 Acre Wood", 265_1654), + "100 Acre Wood Bouncing Spot Left Cliff Chest": KH1LocationData("100 Acre Wood", 265_1691), + "100 Acre Wood Bouncing Spot Right Tree Alcove Chest": KH1LocationData("100 Acre Wood", 265_1692), + "100 Acre Wood Bouncing Spot Under Giant Pot Chest": KH1LocationData("100 Acre Wood", 265_1693), + "Agrabah Plaza By Storage Chest": KH1LocationData("Agrabah", 265_1972), + "Agrabah Plaza Raised Terrace Chest": KH1LocationData("Agrabah", 265_1973), + "Agrabah Plaza Top Corner Chest": KH1LocationData("Agrabah", 265_1974), + "Agrabah Alley Chest": KH1LocationData("Agrabah", 265_2011), + "Agrabah Bazaar Across Windows Chest": KH1LocationData("Agrabah", 265_2012), + "Agrabah Bazaar High Corner Chest": KH1LocationData("Agrabah", 265_2013), + "Agrabah Main Street Right Palace Entrance Chest": KH1LocationData("Agrabah", 265_2014), + "Agrabah Main Street High Above Alley Entrance Chest": KH1LocationData("Agrabah", 265_2051), + "Agrabah Main Street High Above Palace Gates Entrance Chest": KH1LocationData("Agrabah", 265_2052), + "Agrabah Palace Gates Low Chest": KH1LocationData("Agrabah", 265_2053), + "Agrabah Palace Gates High Opposite Palace Chest": KH1LocationData("Agrabah", 265_2054), + "Agrabah Palace Gates High Close to Palace Chest": KH1LocationData("Agrabah", 265_2091), + "Agrabah Storage Green Trinity Chest": KH1LocationData("Agrabah", 265_2092), + "Agrabah Storage Behind Barrel Chest": KH1LocationData("Agrabah", 265_2093), + "Agrabah Cave of Wonders Entrance Left Chest": KH1LocationData("Agrabah", 265_2094), + "Agrabah Cave of Wonders Entrance Tall Tower Chest": KH1LocationData("Agrabah", 265_2131), + "Agrabah Cave of Wonders Hall High Left Chest": KH1LocationData("Agrabah", 265_2132), + "Agrabah Cave of Wonders Hall Near Bottomless Hall Chest": KH1LocationData("Agrabah", 265_2133), + "Agrabah Cave of Wonders Bottomless Hall Raised Platform Chest": KH1LocationData("Agrabah", 265_2134), + "Agrabah Cave of Wonders Bottomless Hall Pillar Chest": KH1LocationData("Agrabah", 265_2171), + "Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest": KH1LocationData("Agrabah", 265_2172), + "Agrabah Cave of Wonders Treasure Room Across Platforms Chest": KH1LocationData("Agrabah", 265_2173), + "Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest": KH1LocationData("Agrabah", 265_2174), + "Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest": KH1LocationData("Agrabah", 265_2211), + "Agrabah Cave of Wonders Treasure Room Above Fire Chest": KH1LocationData("Agrabah", 265_2212), + "Agrabah Cave of Wonders Relic Chamber Jump from Stairs Chest": KH1LocationData("Agrabah", 265_2213), + "Agrabah Cave of Wonders Relic Chamber Stairs Chest": KH1LocationData("Agrabah", 265_2214), + "Agrabah Cave of Wonders Dark Chamber Abu Gem Chest": KH1LocationData("Agrabah", 265_2251), + "Agrabah Cave of Wonders Dark Chamber Across from Relic Chamber Entrance Chest": KH1LocationData("Agrabah", 265_2252), + "Agrabah Cave of Wonders Dark Chamber Bridge Chest": KH1LocationData("Agrabah", 265_2253), + "Agrabah Cave of Wonders Dark Chamber Near Save Chest": KH1LocationData("Agrabah", 265_2254), + "Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest": KH1LocationData("Agrabah", 265_2291), + "Agrabah Cave of Wonders Hidden Room Right Chest": KH1LocationData("Agrabah", 265_2292), + "Agrabah Cave of Wonders Hidden Room Left Chest": KH1LocationData("Agrabah", 265_2293), + "Agrabah Aladdin's House Main Street Entrance Chest": KH1LocationData("Agrabah", 265_2294), + "Agrabah Aladdin's House Plaza Entrance Chest": KH1LocationData("Agrabah", 265_2331), + "Agrabah Cave of Wonders Entrance White Trinity Chest": KH1LocationData("Agrabah", 265_2332), + "Monstro Chamber 6 Other Platform Chest": KH1LocationData("Monstro", 265_2413), + "Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest": KH1LocationData("Monstro", 265_2414), + "Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest": KH1LocationData("Monstro", 265_2451), + "Monstro Chamber 6 Low Chest": KH1LocationData("Monstro", 265_2452), + "Atlantica Sunken Ship In Flipped Boat Chest": KH1LocationData("Atlantica", 265_2531), + "Atlantica Sunken Ship Seabed Chest": KH1LocationData("Atlantica", 265_2532), + "Atlantica Sunken Ship Inside Ship Chest": KH1LocationData("Atlantica", 265_2533), + "Atlantica Ariel's Grotto High Chest": KH1LocationData("Atlantica", 265_2534), + "Atlantica Ariel's Grotto Middle Chest": KH1LocationData("Atlantica", 265_2571), + "Atlantica Ariel's Grotto Low Chest": KH1LocationData("Atlantica", 265_2572), + "Atlantica Ursula's Lair Use Fire on Urchin Chest": KH1LocationData("Atlantica", 265_2573), + "Atlantica Undersea Gorge Jammed by Ariel's Grotto Chest": KH1LocationData("Atlantica", 265_2574), + "Atlantica Triton's Palace White Trinity Chest": KH1LocationData("Atlantica", 265_2611), + "Halloween Town Moonlight Hill White Trinity Chest": KH1LocationData("Halloween Town", 265_3014), + "Halloween Town Bridge Under Bridge": KH1LocationData("Halloween Town", 265_3051), + "Halloween Town Boneyard Tombstone Puzzle Chest": KH1LocationData("Halloween Town", 265_3052), + "Halloween Town Bridge Right of Gate Chest": KH1LocationData("Halloween Town", 265_3053), + "Halloween Town Cemetery Behind Grave Chest": KH1LocationData("Halloween Town", 265_3054), + "Halloween Town Cemetery By Cat Shape Chest": KH1LocationData("Halloween Town", 265_3091), + "Halloween Town Cemetery Between Graves Chest": KH1LocationData("Halloween Town", 265_3092), + "Halloween Town Oogie's Manor Lower Iron Cage Chest": KH1LocationData("Halloween Town", 265_3093), + "Halloween Town Oogie's Manor Upper Iron Cage Chest": KH1LocationData("Halloween Town", 265_3094), + "Halloween Town Oogie's Manor Hollow Chest": KH1LocationData("Halloween Town", 265_3131), + "Halloween Town Oogie's Manor Grounds Red Trinity Chest": KH1LocationData("Halloween Town", 265_3132), + "Halloween Town Guillotine Square High Tower Chest": KH1LocationData("Halloween Town", 265_3133), + "Halloween Town Guillotine Square Pumpkin Structure Left Chest": KH1LocationData("Halloween Town", 265_3134), + "Halloween Town Oogie's Manor Entrance Steps Chest": KH1LocationData("Halloween Town", 265_3171), + "Halloween Town Oogie's Manor Inside Entrance Chest": KH1LocationData("Halloween Town", 265_3172), + "Halloween Town Bridge Left of Gate Chest": KH1LocationData("Halloween Town", 265_3291), + "Halloween Town Cemetery By Striped Grave Chest": KH1LocationData("Halloween Town", 265_3292), + "Halloween Town Guillotine Square Under Jack's House Stairs Chest": KH1LocationData("Halloween Town", 265_3293), + "Halloween Town Guillotine Square Pumpkin Structure Right Chest": KH1LocationData("Halloween Town", 265_3294), + "Olympus Coliseum Coliseum Gates Left Behind Columns Chest": KH1LocationData("Olympus Coliseum", 265_3332), + "Olympus Coliseum Coliseum Gates Right Blue Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3333), + "Olympus Coliseum Coliseum Gates Left Blue Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3334), + "Olympus Coliseum Coliseum Gates White Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3371), + "Olympus Coliseum Coliseum Gates Blizzara Chest": KH1LocationData("Olympus Coliseum", 265_3372), + "Olympus Coliseum Coliseum Gates Blizzaga Chest": KH1LocationData("Olympus Coliseum", 265_3373), + "Monstro Mouth Boat Deck Chest": KH1LocationData("Monstro", 265_3454), + "Monstro Mouth High Platform Boat Side Chest": KH1LocationData("Monstro", 265_3491), + "Monstro Mouth High Platform Across from Boat Chest": KH1LocationData("Monstro", 265_3492), + "Monstro Mouth Near Ship Chest": KH1LocationData("Monstro", 265_3493), + "Monstro Mouth Green Trinity Top of Boat Chest": KH1LocationData("Monstro", 265_3494), + "Monstro Chamber 2 Ground Chest": KH1LocationData("Monstro", 265_3534), + "Monstro Chamber 2 Platform Chest": KH1LocationData("Monstro", 265_3571), + "Monstro Chamber 5 Platform Chest": KH1LocationData("Monstro", 265_3613), + "Monstro Chamber 3 Ground Chest": KH1LocationData("Monstro", 265_3614), + "Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest": KH1LocationData("Monstro", 265_3651), + "Monstro Chamber 3 Near Chamber 6 Entrance Chest": KH1LocationData("Monstro", 265_3652), + "Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest": KH1LocationData("Monstro", 265_3653), + "Monstro Mouth High Platform Near Teeth Chest": KH1LocationData("Monstro", 265_3732), + "Monstro Chamber 5 Atop Barrel Chest": KH1LocationData("Monstro", 265_3733), + "Monstro Chamber 5 Low 2nd Chest": KH1LocationData("Monstro", 265_3734), + "Monstro Chamber 5 Low 1st Chest": KH1LocationData("Monstro", 265_3771), + "Neverland Pirate Ship Deck White Trinity Chest": KH1LocationData("Neverland", 265_3772), + "Neverland Pirate Ship Crows Nest Chest": KH1LocationData("Neverland", 265_3773), + "Neverland Hold Yellow Trinity Right Blue Chest": KH1LocationData("Neverland", 265_3774), + "Neverland Hold Yellow Trinity Left Blue Chest": KH1LocationData("Neverland", 265_3811), + "Neverland Galley Chest": KH1LocationData("Neverland", 265_3812), + "Neverland Cabin Chest": KH1LocationData("Neverland", 265_3813), + "Neverland Hold Flight 1st Chest": KH1LocationData("Neverland", 265_3814), + "Neverland Clock Tower Chest": KH1LocationData("Neverland", 265_4014), + "Neverland Hold Flight 2nd Chest": KH1LocationData("Neverland", 265_4051), + "Neverland Hold Yellow Trinity Green Chest": KH1LocationData("Neverland", 265_4052), + "Neverland Captain's Cabin Chest": KH1LocationData("Neverland", 265_4053), + "Hollow Bastion Rising Falls Water's Surface Chest": KH1LocationData("Hollow Bastion", 265_4054), + "Hollow Bastion Rising Falls Under Water 1st Chest": KH1LocationData("Hollow Bastion", 265_4091), + "Hollow Bastion Rising Falls Under Water 2nd Chest": KH1LocationData("Hollow Bastion", 265_4092), + "Hollow Bastion Rising Falls Floating Platform Near Save Chest": KH1LocationData("Hollow Bastion", 265_4093), + "Hollow Bastion Rising Falls Floating Platform Near Bubble Chest": KH1LocationData("Hollow Bastion", 265_4094), + "Hollow Bastion Rising Falls High Platform Chest": KH1LocationData("Hollow Bastion", 265_4131), + "Hollow Bastion Castle Gates Gravity Chest": KH1LocationData("Hollow Bastion", 265_4132), + "Hollow Bastion Castle Gates Freestanding Pillar Chest": KH1LocationData("Hollow Bastion", 265_4133), + "Hollow Bastion Castle Gates High Pillar Chest": KH1LocationData("Hollow Bastion", 265_4134), + "Hollow Bastion Great Crest Lower Chest": KH1LocationData("Hollow Bastion", 265_4171), + "Hollow Bastion Great Crest After Battle Platform Chest": KH1LocationData("Hollow Bastion", 265_4172), + "Hollow Bastion High Tower 2nd Gravity Chest": KH1LocationData("Hollow Bastion", 265_4173), + "Hollow Bastion High Tower 1st Gravity Chest": KH1LocationData("Hollow Bastion", 265_4174), + "Hollow Bastion High Tower Above Sliding Blocks Chest": KH1LocationData("Hollow Bastion", 265_4211), + "Hollow Bastion Library Top of Bookshelf Chest": KH1LocationData("Hollow Bastion", 265_4213), + "Hollow Bastion Library 1st Floor Turn the Carousel Chest": KH1LocationData("Hollow Bastion", 265_4214), + "Hollow Bastion Library Top of Bookshelf Turn the Carousel Chest": KH1LocationData("Hollow Bastion", 265_4251), + "Hollow Bastion Library 2nd Floor Turn the Carousel 1st Chest": KH1LocationData("Hollow Bastion", 265_4252), + "Hollow Bastion Library 2nd Floor Turn the Carousel 2nd Chest": KH1LocationData("Hollow Bastion", 265_4253), + "Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest": KH1LocationData("Hollow Bastion", 265_4254), + "Hollow Bastion Lift Stop Library Node Gravity Chest": KH1LocationData("Hollow Bastion", 265_4291), + "Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest": KH1LocationData("Hollow Bastion", 265_4292), + "Hollow Bastion Lift Stop Outside Library Gravity Chest": KH1LocationData("Hollow Bastion", 265_4293), + "Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest": KH1LocationData("Hollow Bastion", 265_4294), + "Hollow Bastion Base Level Bubble Under the Wall Platform Chest": KH1LocationData("Hollow Bastion", 265_4331), + "Hollow Bastion Base Level Platform Near Entrance Chest": KH1LocationData("Hollow Bastion", 265_4332), + "Hollow Bastion Base Level Near Crystal Switch Chest": KH1LocationData("Hollow Bastion", 265_4333), + "Hollow Bastion Waterway Near Save Chest": KH1LocationData("Hollow Bastion", 265_4334), + "Hollow Bastion Waterway Blizzard on Bubble Chest": KH1LocationData("Hollow Bastion", 265_4371), + "Hollow Bastion Waterway Unlock Passage from Base Level Chest": KH1LocationData("Hollow Bastion", 265_4372), + "Hollow Bastion Dungeon By Candles Chest": KH1LocationData("Hollow Bastion", 265_4373), + "Hollow Bastion Dungeon Corner Chest": KH1LocationData("Hollow Bastion", 265_4374), + "Hollow Bastion Grand Hall Steps Right Side Chest": KH1LocationData("Hollow Bastion", 265_4454), + "Hollow Bastion Grand Hall Oblivion Chest": KH1LocationData("Hollow Bastion", 265_4491), + "Hollow Bastion Grand Hall Left of Gate Chest": KH1LocationData("Hollow Bastion", 265_4492), + #"Hollow Bastion Entrance Hall Push the Statue Chest": KH1LocationData("Hollow Bastion", 265_4493), --handled later + "Hollow Bastion Entrance Hall Left of Emblem Door Chest": KH1LocationData("Hollow Bastion", 265_4212), + "Hollow Bastion Rising Falls White Trinity Chest": KH1LocationData("Hollow Bastion", 265_4494), + "End of the World Final Dimension 1st Chest": KH1LocationData("End of the World", 265_4531), + "End of the World Final Dimension 2nd Chest": KH1LocationData("End of the World", 265_4532), + "End of the World Final Dimension 3rd Chest": KH1LocationData("End of the World", 265_4533), + "End of the World Final Dimension 4th Chest": KH1LocationData("End of the World", 265_4534), + "End of the World Final Dimension 5th Chest": KH1LocationData("End of the World", 265_4571), + "End of the World Final Dimension 6th Chest": KH1LocationData("End of the World", 265_4572), + "End of the World Final Dimension 10th Chest": KH1LocationData("End of the World", 265_4573), + "End of the World Final Dimension 9th Chest": KH1LocationData("End of the World", 265_4574), + "End of the World Final Dimension 8th Chest": KH1LocationData("End of the World", 265_4611), + "End of the World Final Dimension 7th Chest": KH1LocationData("End of the World", 265_4612), + "End of the World Giant Crevasse 3rd Chest": KH1LocationData("End of the World", 265_4613), + "End of the World Giant Crevasse 5th Chest": KH1LocationData("End of the World", 265_4614), + "End of the World Giant Crevasse 1st Chest": KH1LocationData("End of the World", 265_4651), + "End of the World Giant Crevasse 4th Chest": KH1LocationData("End of the World", 265_4652), + "End of the World Giant Crevasse 2nd Chest": KH1LocationData("End of the World", 265_4653), + "End of the World World Terminus Traverse Town Chest": KH1LocationData("End of the World", 265_4654), + "End of the World World Terminus Wonderland Chest": KH1LocationData("End of the World", 265_4691), + "End of the World World Terminus Olympus Coliseum Chest": KH1LocationData("End of the World", 265_4692), + "End of the World World Terminus Deep Jungle Chest": KH1LocationData("End of the World", 265_4693), + "End of the World World Terminus Agrabah Chest": KH1LocationData("End of the World", 265_4694), + "End of the World World Terminus Atlantica Chest": KH1LocationData("End of the World", 265_4731), + "End of the World World Terminus Halloween Town Chest": KH1LocationData("End of the World", 265_4732), + "End of the World World Terminus Neverland Chest": KH1LocationData("End of the World", 265_4733), + "End of the World World Terminus 100 Acre Wood Chest": KH1LocationData("End of the World", 265_4734), + #"End of the World World Terminus Hollow Bastion Chest": KH1LocationData("End of the World", 265_4771), + "End of the World Final Rest Chest": KH1LocationData("End of the World", 265_4772), + "Monstro Chamber 6 White Trinity Chest": KH1LocationData("End of the World", 265_5092), + #"Awakening Chest": KH1LocationData("Awakening", 265_5093), missable + + "Traverse Town Defeat Guard Armor Dodge Roll Event": KH1LocationData("Traverse Town", 265_6011), + "Traverse Town Defeat Guard Armor Fire Event": KH1LocationData("Traverse Town", 265_6012), + "Traverse Town Defeat Guard Armor Blue Trinity Event": KH1LocationData("Traverse Town", 265_6013), + "Traverse Town Leon Secret Waterway Earthshine Event": KH1LocationData("Traverse Town", 265_6014), + "Traverse Town Kairi Secret Waterway Oathkeeper Event": KH1LocationData("Traverse Town", 265_6015), + "Traverse Town Defeat Guard Armor Brave Warrior Event": KH1LocationData("Traverse Town", 265_6016), + "Deep Jungle Defeat Sabor White Fang Event": KH1LocationData("Deep Jungle", 265_6021), + "Deep Jungle Defeat Clayton Cure Event": KH1LocationData("Deep Jungle", 265_6022), + "Deep Jungle Seal Keyhole Jungle King Event": KH1LocationData("Deep Jungle", 265_6023), + "Deep Jungle Seal Keyhole Red Trinity Event": KH1LocationData("Deep Jungle", 265_6024), + "Olympus Coliseum Clear Phil's Training Thunder Event": KH1LocationData("Olympus Coliseum", 265_6031), + "Olympus Coliseum Defeat Cerberus Inferno Band Event": KH1LocationData("Olympus Coliseum", 265_6033), + "Wonderland Defeat Trickmaster Blizzard Event": KH1LocationData("Wonderland", 265_6041), + "Wonderland Defeat Trickmaster Ifrit's Horn Event": KH1LocationData("Wonderland", 265_6042), + "Agrabah Defeat Pot Centipede Ray of Light Event": KH1LocationData("Agrabah", 265_6051), + "Agrabah Defeat Jafar Blizzard Event": KH1LocationData("Agrabah", 265_6052), + "Agrabah Defeat Jafar Genie Fire Event": KH1LocationData("Agrabah", 265_6053), + "Agrabah Seal Keyhole Genie Event": KH1LocationData("Agrabah", 265_6054), + "Agrabah Seal Keyhole Three Wishes Event": KH1LocationData("Agrabah", 265_6055), + "Agrabah Seal Keyhole Green Trinity Event": KH1LocationData("Agrabah", 265_6056), + "Monstro Defeat Parasite Cage I Goofy Cheer Event": KH1LocationData("Monstro", 265_6061), + "Monstro Defeat Parasite Cage II Stop Event": KH1LocationData("Monstro", 265_6062), + "Atlantica Defeat Ursula I Mermaid Kick Event": KH1LocationData("Atlantica", 265_6071), + "Atlantica Defeat Ursula II Thunder Event": KH1LocationData("Atlantica", 265_6072), + "Atlantica Seal Keyhole Crabclaw Event": KH1LocationData("Atlantica", 265_6073), + "Halloween Town Defeat Oogie Boogie Holy Circlet Event": KH1LocationData("Halloween Town", 265_6081), + "Halloween Town Defeat Oogie's Manor Gravity Event": KH1LocationData("Halloween Town", 265_6082), + "Halloween Town Seal Keyhole Pumpkinhead Event": KH1LocationData("Halloween Town", 265_6083), + "Neverland Defeat Anti Sora Raven's Claw Event": KH1LocationData("Neverland", 265_6091), + "Neverland Encounter Hook Cure Event": KH1LocationData("Neverland", 265_6092), + "Neverland Seal Keyhole Fairy Harp Event": KH1LocationData("Neverland", 265_6093), + "Neverland Seal Keyhole Tinker Bell Event": KH1LocationData("Neverland", 265_6094), + "Neverland Seal Keyhole Glide Event": KH1LocationData("Neverland", 265_6095), + "Neverland Defeat Phantom Stop Event": KH1LocationData("Neverland", 265_6096), + "Neverland Defeat Captain Hook Ars Arcanum Event": KH1LocationData("Neverland", 265_6097), + "Hollow Bastion Defeat Riku I White Trinity Event": KH1LocationData("Hollow Bastion", 265_6101), + "Hollow Bastion Defeat Maleficent Donald Cheer Event": KH1LocationData("Hollow Bastion", 265_6102), + "Hollow Bastion Defeat Dragon Maleficent Fireglow Event": KH1LocationData("Hollow Bastion", 265_6103), + "Hollow Bastion Defeat Riku II Ragnarok Event": KH1LocationData("Hollow Bastion", 265_6104), + "Hollow Bastion Defeat Behemoth Omega Arts Event": KH1LocationData("Hollow Bastion", 265_6105), + "Hollow Bastion Speak to Princesses Fire Event": KH1LocationData("Hollow Bastion", 265_6106), + "End of the World Defeat Chernabog Superglide Event": KH1LocationData("End of the World", 265_6111), + + "Traverse Town Mail Postcard 01 Event": KH1LocationData("Traverse Town", 265_6120), + "Traverse Town Mail Postcard 02 Event": KH1LocationData("Traverse Town", 265_6121), + "Traverse Town Mail Postcard 03 Event": KH1LocationData("Traverse Town", 265_6122), + "Traverse Town Mail Postcard 04 Event": KH1LocationData("Traverse Town", 265_6123), + "Traverse Town Mail Postcard 05 Event": KH1LocationData("Traverse Town", 265_6124), + "Traverse Town Mail Postcard 06 Event": KH1LocationData("Traverse Town", 265_6125), + "Traverse Town Mail Postcard 07 Event": KH1LocationData("Traverse Town", 265_6126), + "Traverse Town Mail Postcard 08 Event": KH1LocationData("Traverse Town", 265_6127), + "Traverse Town Mail Postcard 09 Event": KH1LocationData("Traverse Town", 265_6128), + "Traverse Town Mail Postcard 10 Event": KH1LocationData("Traverse Town", 265_6129), + + "Traverse Town Defeat Opposite Armor Aero Event": KH1LocationData("Traverse Town", 265_6131), + + "Atlantica Undersea Gorge Blizzard Clam": KH1LocationData("Atlantica", 265_6201), + "Atlantica Undersea Gorge Ocean Floor Clam": KH1LocationData("Atlantica", 265_6202), + "Atlantica Undersea Valley Higher Cave Clam": KH1LocationData("Atlantica", 265_6203), + "Atlantica Undersea Valley Lower Cave Clam": KH1LocationData("Atlantica", 265_6204), + "Atlantica Undersea Valley Fire Clam": KH1LocationData("Atlantica", 265_6205), + "Atlantica Undersea Valley Wall Clam": KH1LocationData("Atlantica", 265_6206), + "Atlantica Undersea Valley Pillar Clam": KH1LocationData("Atlantica", 265_6207), + "Atlantica Undersea Valley Ocean Floor Clam": KH1LocationData("Atlantica", 265_6208), + "Atlantica Triton's Palace Thunder Clam": KH1LocationData("Atlantica", 265_6209), + "Atlantica Triton's Palace Wall Right Clam": KH1LocationData("Atlantica", 265_6210), + "Atlantica Triton's Palace Near Path Clam": KH1LocationData("Atlantica", 265_6211), + "Atlantica Triton's Palace Wall Left Clam": KH1LocationData("Atlantica", 265_6212), + "Atlantica Cavern Nook Clam": KH1LocationData("Atlantica", 265_6213), + "Atlantica Below Deck Clam": KH1LocationData("Atlantica", 265_6214), + "Atlantica Undersea Garden Clam": KH1LocationData("Atlantica", 265_6215), + "Atlantica Undersea Cave Clam": KH1LocationData("Atlantica", 265_6216), + + #"Traverse Town Magician's Study Turn in Naturespark": KH1LocationData("Traverse Town", 265_6300), + #"Traverse Town Magician's Study Turn in Watergleam": KH1LocationData("Traverse Town", 265_6301), + #"Traverse Town Magician's Study Turn in Fireglow": KH1LocationData("Traverse Town", 265_6302), + #"Traverse Town Magician's Study Turn in all Summon Gems": KH1LocationData("Traverse Town", 265_6303), + "Traverse Town Geppetto's House Geppetto Reward 1": KH1LocationData("Traverse Town", 265_6304), + "Traverse Town Geppetto's House Geppetto Reward 2": KH1LocationData("Traverse Town", 265_6305), + "Traverse Town Geppetto's House Geppetto Reward 3": KH1LocationData("Traverse Town", 265_6306), + "Traverse Town Geppetto's House Geppetto Reward 4": KH1LocationData("Traverse Town", 265_6307), + "Traverse Town Geppetto's House Geppetto Reward 5": KH1LocationData("Traverse Town", 265_6308), + "Traverse Town Geppetto's House Geppetto All Summons Reward": KH1LocationData("Traverse Town", 265_6309), + "Traverse Town Geppetto's House Talk to Pinocchio": KH1LocationData("Traverse Town", 265_6310), + "Traverse Town Magician's Study Obtained All Arts Items": KH1LocationData("Traverse Town", 265_6311), + "Traverse Town Magician's Study Obtained All LV1 Magic": KH1LocationData("Traverse Town", 265_6312), + "Traverse Town Magician's Study Obtained All LV3 Magic": KH1LocationData("Traverse Town", 265_6313), + "Traverse Town Piano Room Return 10 Puppies": KH1LocationData("Traverse Town", 265_6314), + "Traverse Town Piano Room Return 20 Puppies": KH1LocationData("Traverse Town", 265_6315), + "Traverse Town Piano Room Return 30 Puppies": KH1LocationData("Traverse Town", 265_6316), + "Traverse Town Piano Room Return 40 Puppies": KH1LocationData("Traverse Town", 265_6317), + "Traverse Town Piano Room Return 50 Puppies Reward 1": KH1LocationData("Traverse Town", 265_6318), + "Traverse Town Piano Room Return 50 Puppies Reward 2": KH1LocationData("Traverse Town", 265_6319), + "Traverse Town Piano Room Return 60 Puppies": KH1LocationData("Traverse Town", 265_6320), + "Traverse Town Piano Room Return 70 Puppies": KH1LocationData("Traverse Town", 265_6321), + "Traverse Town Piano Room Return 80 Puppies": KH1LocationData("Traverse Town", 265_6322), + "Traverse Town Piano Room Return 90 Puppies": KH1LocationData("Traverse Town", 265_6324), + "Traverse Town Piano Room Return 99 Puppies Reward 1": KH1LocationData("Traverse Town", 265_6326), + "Traverse Town Piano Room Return 99 Puppies Reward 2": KH1LocationData("Traverse Town", 265_6327), + "Olympus Coliseum Cloud Sonic Blade Event": KH1LocationData("Olympus Coliseum", 265_6032), #Had to change the way we send this check, not changing location_id + "Olympus Coliseum Defeat Sephiroth One-Winged Angel Event": KH1LocationData("Olympus Coliseum", 265_6328), + "Olympus Coliseum Defeat Ice Titan Diamond Dust Event": KH1LocationData("Olympus Coliseum", 265_6329), + "Olympus Coliseum Gates Purple Jar After Defeating Hades": KH1LocationData("Olympus Coliseum", 265_6330), + "Halloween Town Guillotine Square Ring Jack's Doorbell 3 Times": KH1LocationData("Halloween Town", 265_6331), + #"Neverland Clock Tower 01:00 Door": KH1LocationData("Neverland", 265_6332), + #"Neverland Clock Tower 02:00 Door": KH1LocationData("Neverland", 265_6333), + #"Neverland Clock Tower 03:00 Door": KH1LocationData("Neverland", 265_6334), + #"Neverland Clock Tower 04:00 Door": KH1LocationData("Neverland", 265_6335), + #"Neverland Clock Tower 05:00 Door": KH1LocationData("Neverland", 265_6336), + #"Neverland Clock Tower 06:00 Door": KH1LocationData("Neverland", 265_6337), + #"Neverland Clock Tower 07:00 Door": KH1LocationData("Neverland", 265_6338), + #"Neverland Clock Tower 08:00 Door": KH1LocationData("Neverland", 265_6339), + #"Neverland Clock Tower 09:00 Door": KH1LocationData("Neverland", 265_6340), + #"Neverland Clock Tower 10:00 Door": KH1LocationData("Neverland", 265_6341), + #"Neverland Clock Tower 11:00 Door": KH1LocationData("Neverland", 265_6342), + #"Neverland Clock Tower 12:00 Door": KH1LocationData("Neverland", 265_6343), + "Neverland Hold Aero Chest": KH1LocationData("Neverland", 265_6344), + "100 Acre Wood Bouncing Spot Turn in Rare Nut 1": KH1LocationData("100 Acre Wood", 265_6345), + "100 Acre Wood Bouncing Spot Turn in Rare Nut 2": KH1LocationData("100 Acre Wood", 265_6346), + "100 Acre Wood Bouncing Spot Turn in Rare Nut 3": KH1LocationData("100 Acre Wood", 265_6347), + "100 Acre Wood Bouncing Spot Turn in Rare Nut 4": KH1LocationData("100 Acre Wood", 265_6348), + "100 Acre Wood Bouncing Spot Turn in Rare Nut 5": KH1LocationData("100 Acre Wood", 265_6349), + "100 Acre Wood Pooh's House Owl Cheer": KH1LocationData("100 Acre Wood", 265_6350), + "100 Acre Wood Convert Torn Page 1": KH1LocationData("100 Acre Wood", 265_6351), + "100 Acre Wood Convert Torn Page 2": KH1LocationData("100 Acre Wood", 265_6352), + "100 Acre Wood Convert Torn Page 3": KH1LocationData("100 Acre Wood", 265_6353), + "100 Acre Wood Convert Torn Page 4": KH1LocationData("100 Acre Wood", 265_6354), + "100 Acre Wood Convert Torn Page 5": KH1LocationData("100 Acre Wood", 265_6355), + "100 Acre Wood Pooh's House Start Fire": KH1LocationData("100 Acre Wood", 265_6356), + "100 Acre Wood Pooh's Room Cabinet": KH1LocationData("100 Acre Wood", 265_6357), + "100 Acre Wood Pooh's Room Chimney": KH1LocationData("100 Acre Wood", 265_6358), + "100 Acre Wood Bouncing Spot Break Log": KH1LocationData("100 Acre Wood", 265_6359), + "100 Acre Wood Bouncing Spot Fall Through Top of Tree Next to Pooh": KH1LocationData("100 Acre Wood", 265_6360), + "Deep Jungle Camp Hi-Potion Experiment": KH1LocationData("Deep Jungle", 265_6361), + "Deep Jungle Camp Ether Experiment": KH1LocationData("Deep Jungle", 265_6362), + "Deep Jungle Camp Replication Experiment": KH1LocationData("Deep Jungle", 265_6363), + "Deep Jungle Cliff Save Gorillas": KH1LocationData("Deep Jungle", 265_6364), + "Deep Jungle Tree House Save Gorillas": KH1LocationData("Deep Jungle", 265_6365), + "Deep Jungle Camp Save Gorillas": KH1LocationData("Deep Jungle", 265_6366), + "Deep Jungle Bamboo Thicket Save Gorillas": KH1LocationData("Deep Jungle", 265_6367), + "Deep Jungle Climbing Trees Save Gorillas": KH1LocationData("Deep Jungle", 265_6368), + "Olympus Coliseum Olympia Chest": KH1LocationData("Olympus Coliseum", 265_6369), + "Deep Jungle Jungle Slider 10 Fruits": KH1LocationData("Deep Jungle", 265_6370), + "Deep Jungle Jungle Slider 20 Fruits": KH1LocationData("Deep Jungle", 265_6371), + "Deep Jungle Jungle Slider 30 Fruits": KH1LocationData("Deep Jungle", 265_6372), + "Deep Jungle Jungle Slider 40 Fruits": KH1LocationData("Deep Jungle", 265_6373), + "Deep Jungle Jungle Slider 50 Fruits": KH1LocationData("Deep Jungle", 265_6374), + "Traverse Town 1st District Speak with Cid Event": KH1LocationData("Traverse Town", 265_6375), + "Wonderland Bizarre Room Read Book": KH1LocationData("Wonderland", 265_6376), + "Olympus Coliseum Coliseum Gates Green Trinity": KH1LocationData("Olympus Coliseum", 265_6377), + "Agrabah Defeat Kurt Zisa Zantetsuken Event": KH1LocationData("Agrabah", 265_6378), + "Hollow Bastion Defeat Unknown EXP Necklace Event": KH1LocationData("Hollow Bastion", 265_6379), + "Olympus Coliseum Coliseum Gates Hero's License Event": KH1LocationData("Olympus Coliseum", 265_6380), + "Atlantica Sunken Ship Crystal Trident Event": KH1LocationData("Atlantica", 265_6381), + "Halloween Town Graveyard Forget-Me-Not Event": KH1LocationData("Halloween Town", 265_6382), + "Deep Jungle Tent Protect-G Event": KH1LocationData("Deep Jungle", 265_6383), + "Deep Jungle Cavern of Hearts Navi-G Piece Event": KH1LocationData("Deep Jungle", 265_6384), + "Wonderland Bizarre Room Navi-G Piece Event": KH1LocationData("Wonderland", 265_6385), + "Olympus Coliseum Coliseum Gates Entry Pass Event": KH1LocationData("Olympus Coliseum", 265_6386), + + "Traverse Town Synth Log": KH1LocationData("Traverse Town", 265_6401), + "Traverse Town Synth Cloth": KH1LocationData("Traverse Town", 265_6402), + "Traverse Town Synth Rope": KH1LocationData("Traverse Town", 265_6403), + "Traverse Town Synth Seagull Egg": KH1LocationData("Traverse Town", 265_6404), + "Traverse Town Synth Fish": KH1LocationData("Traverse Town", 265_6405), + "Traverse Town Synth Mushroom": KH1LocationData("Traverse Town", 265_6406), + + "Traverse Town Item Shop Postcard": KH1LocationData("Traverse Town", 265_6500), + "Traverse Town 1st District Safe Postcard": KH1LocationData("Traverse Town", 265_6501), + "Traverse Town Gizmo Shop Postcard 1": KH1LocationData("Traverse Town", 265_6502), + "Traverse Town Gizmo Shop Postcard 2": KH1LocationData("Traverse Town", 265_6503), + "Traverse Town Item Workshop Postcard": KH1LocationData("Traverse Town", 265_6504), + "Traverse Town 3rd District Balcony Postcard": KH1LocationData("Traverse Town", 265_6505), + "Traverse Town Geppetto's House Postcard": KH1LocationData("Traverse Town", 265_6506), + "Halloween Town Lab Torn Page": KH1LocationData("Halloween Town", 265_6508), + "Hollow Bastion Entrance Hall Emblem Piece (Flame)": KH1LocationData("Hollow Bastion", 265_6516), + "Hollow Bastion Entrance Hall Emblem Piece (Chest)": KH1LocationData("Hollow Bastion", 265_6517), + "Hollow Bastion Entrance Hall Emblem Piece (Statue)": KH1LocationData("Hollow Bastion", 265_6518), + "Hollow Bastion Entrance Hall Emblem Piece (Fountain)": KH1LocationData("Hollow Bastion", 265_6519), + #"Traverse Town 1st District Leon Gift": KH1LocationData("Traverse Town", 265_6520), + #"Traverse Town 1st District Aerith Gift": KH1LocationData("Traverse Town", 265_6521), + "Hollow Bastion Library Speak to Belle Divine Rose": KH1LocationData("Hollow Bastion", 265_6522), + "Hollow Bastion Library Speak to Aerith Cure": KH1LocationData("Hollow Bastion", 265_6523), + + "Agrabah Defeat Jafar Genie Ansem's Report 1": KH1LocationData("Agrabah", 265_7018), + "Hollow Bastion Speak with Aerith Ansem's Report 2": KH1LocationData("Hollow Bastion", 265_7017), + "Atlantica Defeat Ursula II Ansem's Report 3": KH1LocationData("Atlantica", 265_7016), + "Hollow Bastion Speak with Aerith Ansem's Report 4": KH1LocationData("Hollow Bastion", 265_7015), + "Hollow Bastion Defeat Maleficent Ansem's Report 5": KH1LocationData("Hollow Bastion", 265_7014), + "Hollow Bastion Speak with Aerith Ansem's Report 6": KH1LocationData("Hollow Bastion", 265_7013), + "Halloween Town Defeat Oogie Boogie Ansem's Report 7": KH1LocationData("Halloween Town", 265_7012), + "Olympus Coliseum Defeat Hades Ansem's Report 8": KH1LocationData("Olympus Coliseum", 265_7011), + "Neverland Defeat Hook Ansem's Report 9": KH1LocationData("Neverland", 265_7028), + "Hollow Bastion Speak with Aerith Ansem's Report 10": KH1LocationData("Hollow Bastion", 265_7027), + "Agrabah Defeat Kurt Zisa Ansem's Report 11": KH1LocationData("Agrabah", 265_7026), + "Olympus Coliseum Defeat Sephiroth Ansem's Report 12": KH1LocationData("Olympus Coliseum", 265_7025), + "Hollow Bastion Defeat Unknown Ansem's Report 13": KH1LocationData("Hollow Bastion", 265_7024), + "Level 001": KH1LocationData("Levels", 265_8001), + "Level 002": KH1LocationData("Levels", 265_8002), + "Level 003": KH1LocationData("Levels", 265_8003), + "Level 004": KH1LocationData("Levels", 265_8004), + "Level 005": KH1LocationData("Levels", 265_8005), + "Level 006": KH1LocationData("Levels", 265_8006), + "Level 007": KH1LocationData("Levels", 265_8007), + "Level 008": KH1LocationData("Levels", 265_8008), + "Level 009": KH1LocationData("Levels", 265_8009), + "Level 010": KH1LocationData("Levels", 265_8010), + "Level 011": KH1LocationData("Levels", 265_8011), + "Level 012": KH1LocationData("Levels", 265_8012), + "Level 013": KH1LocationData("Levels", 265_8013), + "Level 014": KH1LocationData("Levels", 265_8014), + "Level 015": KH1LocationData("Levels", 265_8015), + "Level 016": KH1LocationData("Levels", 265_8016), + "Level 017": KH1LocationData("Levels", 265_8017), + "Level 018": KH1LocationData("Levels", 265_8018), + "Level 019": KH1LocationData("Levels", 265_8019), + "Level 020": KH1LocationData("Levels", 265_8020), + "Level 021": KH1LocationData("Levels", 265_8021), + "Level 022": KH1LocationData("Levels", 265_8022), + "Level 023": KH1LocationData("Levels", 265_8023), + "Level 024": KH1LocationData("Levels", 265_8024), + "Level 025": KH1LocationData("Levels", 265_8025), + "Level 026": KH1LocationData("Levels", 265_8026), + "Level 027": KH1LocationData("Levels", 265_8027), + "Level 028": KH1LocationData("Levels", 265_8028), + "Level 029": KH1LocationData("Levels", 265_8029), + "Level 030": KH1LocationData("Levels", 265_8030), + "Level 031": KH1LocationData("Levels", 265_8031), + "Level 032": KH1LocationData("Levels", 265_8032), + "Level 033": KH1LocationData("Levels", 265_8033), + "Level 034": KH1LocationData("Levels", 265_8034), + "Level 035": KH1LocationData("Levels", 265_8035), + "Level 036": KH1LocationData("Levels", 265_8036), + "Level 037": KH1LocationData("Levels", 265_8037), + "Level 038": KH1LocationData("Levels", 265_8038), + "Level 039": KH1LocationData("Levels", 265_8039), + "Level 040": KH1LocationData("Levels", 265_8040), + "Level 041": KH1LocationData("Levels", 265_8041), + "Level 042": KH1LocationData("Levels", 265_8042), + "Level 043": KH1LocationData("Levels", 265_8043), + "Level 044": KH1LocationData("Levels", 265_8044), + "Level 045": KH1LocationData("Levels", 265_8045), + "Level 046": KH1LocationData("Levels", 265_8046), + "Level 047": KH1LocationData("Levels", 265_8047), + "Level 048": KH1LocationData("Levels", 265_8048), + "Level 049": KH1LocationData("Levels", 265_8049), + "Level 050": KH1LocationData("Levels", 265_8050), + "Level 051": KH1LocationData("Levels", 265_8051), + "Level 052": KH1LocationData("Levels", 265_8052), + "Level 053": KH1LocationData("Levels", 265_8053), + "Level 054": KH1LocationData("Levels", 265_8054), + "Level 055": KH1LocationData("Levels", 265_8055), + "Level 056": KH1LocationData("Levels", 265_8056), + "Level 057": KH1LocationData("Levels", 265_8057), + "Level 058": KH1LocationData("Levels", 265_8058), + "Level 059": KH1LocationData("Levels", 265_8059), + "Level 060": KH1LocationData("Levels", 265_8060), + "Level 061": KH1LocationData("Levels", 265_8061), + "Level 062": KH1LocationData("Levels", 265_8062), + "Level 063": KH1LocationData("Levels", 265_8063), + "Level 064": KH1LocationData("Levels", 265_8064), + "Level 065": KH1LocationData("Levels", 265_8065), + "Level 066": KH1LocationData("Levels", 265_8066), + "Level 067": KH1LocationData("Levels", 265_8067), + "Level 068": KH1LocationData("Levels", 265_8068), + "Level 069": KH1LocationData("Levels", 265_8069), + "Level 070": KH1LocationData("Levels", 265_8070), + "Level 071": KH1LocationData("Levels", 265_8071), + "Level 072": KH1LocationData("Levels", 265_8072), + "Level 073": KH1LocationData("Levels", 265_8073), + "Level 074": KH1LocationData("Levels", 265_8074), + "Level 075": KH1LocationData("Levels", 265_8075), + "Level 076": KH1LocationData("Levels", 265_8076), + "Level 077": KH1LocationData("Levels", 265_8077), + "Level 078": KH1LocationData("Levels", 265_8078), + "Level 079": KH1LocationData("Levels", 265_8079), + "Level 080": KH1LocationData("Levels", 265_8080), + "Level 081": KH1LocationData("Levels", 265_8081), + "Level 082": KH1LocationData("Levels", 265_8082), + "Level 083": KH1LocationData("Levels", 265_8083), + "Level 084": KH1LocationData("Levels", 265_8084), + "Level 085": KH1LocationData("Levels", 265_8085), + "Level 086": KH1LocationData("Levels", 265_8086), + "Level 087": KH1LocationData("Levels", 265_8087), + "Level 088": KH1LocationData("Levels", 265_8088), + "Level 089": KH1LocationData("Levels", 265_8089), + "Level 090": KH1LocationData("Levels", 265_8090), + "Level 091": KH1LocationData("Levels", 265_8091), + "Level 092": KH1LocationData("Levels", 265_8092), + "Level 093": KH1LocationData("Levels", 265_8093), + "Level 094": KH1LocationData("Levels", 265_8094), + "Level 095": KH1LocationData("Levels", 265_8095), + "Level 096": KH1LocationData("Levels", 265_8096), + "Level 097": KH1LocationData("Levels", 265_8097), + "Level 098": KH1LocationData("Levels", 265_8098), + "Level 099": KH1LocationData("Levels", 265_8099), + "Level 100": KH1LocationData("Levels", 265_8100), + "Complete Phil Cup": KH1LocationData("Olympus Coliseum", 265_9001), + "Complete Phil Cup Solo": KH1LocationData("Olympus Coliseum", 265_9002), + "Complete Phil Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9003), + "Complete Pegasus Cup": KH1LocationData("Olympus Coliseum", 265_9004), + "Complete Pegasus Cup Solo": KH1LocationData("Olympus Coliseum", 265_9005), + "Complete Pegasus Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9006), + "Complete Hercules Cup": KH1LocationData("Olympus Coliseum", 265_9007), + "Complete Hercules Cup Solo": KH1LocationData("Olympus Coliseum", 265_9008), + "Complete Hercules Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9009), + "Complete Hades Cup": KH1LocationData("Olympus Coliseum", 265_9010), + "Complete Hades Cup Solo": KH1LocationData("Olympus Coliseum", 265_9011), + "Complete Hades Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9012), + "Hades Cup Defeat Cloud and Leon Event": KH1LocationData("Olympus Coliseum", 265_9013), + "Hades Cup Defeat Yuffie Event": KH1LocationData("Olympus Coliseum", 265_9014), + "Hades Cup Defeat Cerberus Event": KH1LocationData("Olympus Coliseum", 265_9015), + "Hades Cup Defeat Behemoth Event": KH1LocationData("Olympus Coliseum", 265_9016), + "Hades Cup Defeat Hades Event": KH1LocationData("Olympus Coliseum", 265_9017), + "Hercules Cup Defeat Cloud Event": KH1LocationData("Olympus Coliseum", 265_9018), + "Hercules Cup Yellow Trinity Event": KH1LocationData("Olympus Coliseum", 265_9019), + "Final Ansem": KH1LocationData("Final", 265_9999) +} + +event_location_table: Dict[str, KH1LocationData] = {} + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if data.code} + + +#Make location categories +location_name_groups: Dict[str, Set[str]] = {} +for location in location_table.keys(): + category = location_table[location].category + if category not in location_name_groups.keys(): + location_name_groups[category] = set() + location_name_groups[category].add(location) diff --git a/worlds/kh1/Options.py b/worlds/kh1/Options.py new file mode 100644 index 000000000000..63732f61b2d0 --- /dev/null +++ b/worlds/kh1/Options.py @@ -0,0 +1,445 @@ +from dataclasses import dataclass + +from Options import NamedRange, Choice, Range, Toggle, DefaultOnToggle, PerGameCommonOptions, StartInventoryPool, OptionGroup + +class StrengthIncrease(Range): + """ + Determines the number of Strength Increases to add to the multiworld. + + The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld. + Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random. + """ + display_name = "STR Increases" + range_start = 0 + range_end = 100 + default = 24 + +class DefenseIncrease(Range): + """ + Determines the number of Defense Increases to add to the multiworld. + + The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld. + Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random. + """ + display_name = "DEF Increases" + range_start = 0 + range_end = 100 + default = 24 + +class HPIncrease(Range): + """ + Determines the number of HP Increases to add to the multiworld. + + The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld. + Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random. + """ + display_name = "HP Increases" + range_start = 0 + range_end = 100 + default = 23 + +class APIncrease(Range): + """ + Determines the number of AP Increases to add to the multiworld. + + The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld. + Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random. + """ + display_name = "AP Increases" + range_start = 0 + range_end = 100 + default = 18 + +class MPIncrease(Range): + """ + Determines the number of MP Increases to add to the multiworld. + + The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld. + Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random. + """ + display_name = "MP Increases" + range_start = 0 + range_end = 20 + default = 7 + +class AccessorySlotIncrease(Range): + """ + Determines the number of Accessory Slot Increases to add to the multiworld. + + The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld. + Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random. + """ + display_name = "Accessory Slot Increases" + range_start = 0 + range_end = 6 + default = 1 + +class ItemSlotIncrease(Range): + """ + Determines the number of Item Slot Increases to add to the multiworld. + + The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld. + Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random. + """ + display_name = "Item Slot Increases" + range_start = 0 + range_end = 5 + default = 3 + +class Atlantica(Toggle): + """ + Toggle whether to include checks in Atlantica. + """ + display_name = "Atlantica" + +class HundredAcreWood(Toggle): + """ + Toggle whether to include checks in the 100 Acre Wood. + """ + display_name = "100 Acre Wood" + +class SuperBosses(Toggle): + """ + Toggle whether to include checks behind Super Bosses. + """ + display_name = "Super Bosses" + +class Cups(Toggle): + """ + Toggle whether to include checks behind completing Phil, Pegasus, Hercules, or Hades cups. + Please note that the cup items will still appear in the multiworld even if toggled off, as they are required to challenge Sephiroth. + """ + display_name = "Cups" + +class Goal(Choice): + """ + Determines when victory is achieved in your playthrough. + + Sephiroth: Defeat Sephiroth + Unknown: Defeat Unknown + Postcards: Turn in all 10 postcards in Traverse Town + Final Ansem: Enter End of the World and defeat Ansem as normal + Puppies: Rescue and return all 99 puppies in Traverse Town + Final Rest: Open the chest in End of the World Final Rest + """ + display_name = "Goal" + option_sephiroth = 0 + option_unknown = 1 + option_postcards = 2 + option_final_ansem = 3 + option_puppies = 4 + option_final_rest = 5 + default = 3 + +class EndoftheWorldUnlock(Choice): + """Determines how End of the World is unlocked. + + Item: You can receive an item called "End of the World" which unlocks the world + Reports: A certain amount of reports are required to unlock End of the World, which is defined in your options""" + display_name = "End of the World Unlock" + option_item = 0 + option_reports = 1 + default = 1 + +class FinalRestDoor(Choice): + """Determines what conditions need to be met to manifest the door in Final Rest, allowing the player to challenge Ansem. + + Reports: A certain number of Ansem's Reports are required, determined by the "Reports to Open Final Rest Door" option + Puppies: Having all 99 puppies is required + Postcards: Turning in all 10 postcards is required + Superbosses: Defeating Sephiroth, Unknown, Kurt Zisa, and Phantom are required + """ + display_name = "Final Rest Door" + option_reports = 0 + option_puppies = 1 + option_postcards = 2 + option_superbosses = 3 + +class Puppies(Choice): + """ + Determines how dalmatian puppies are shuffled into the pool. + Full: All puppies are in one location + Triplets: Puppies are found in triplets just as they are in the base game + Individual: One puppy can be found per location + """ + display_name = "Puppies" + option_full = 0 + option_triplets = 1 + option_individual = 2 + default = 1 + +class EXPMultiplier(NamedRange): + """ + Determines the multiplier to apply to EXP gained. + """ + display_name = "EXP Multiplier" + default = 16 + range_start = default // 4 + range_end = 128 + special_range_names = { + "0.25x": int(default // 4), + "0.5x": int(default // 2), + "1x": default, + "2x": default * 2, + "3x": default * 3, + "4x": default * 4, + "8x": default * 8, + } + +class RequiredReportsEotW(Range): + """ + If End of the World Unlock is set to "Reports", determines the number of Ansem's Reports required to open End of the World. + """ + display_name = "Reports to Open End of the World" + default = 4 + range_start = 0 + range_end = 13 + +class RequiredReportsDoor(Range): + """ + If Final Rest Door is set to "Reports", determines the number of Ansem's Reports required to manifest the door in Final Rest to challenge Ansem. + """ + display_name = "Reports to Open Final Rest Door" + default = 4 + range_start = 0 + range_end = 13 + +class ReportsInPool(Range): + """ + Determines the number of Ansem's Reports in the item pool. + """ + display_name = "Reports in Pool" + default = 4 + range_start = 0 + range_end = 13 + +class RandomizeKeybladeStats(DefaultOnToggle): + """ + Determines whether Keyblade stats should be randomized. + """ + display_name = "Randomize Keyblade Stats" + +class KeybladeMinStrength(Range): + """ + Determines the minimum STR bonus a keyblade can have. + """ + display_name = "Keyblade Minimum STR Bonus" + default = 3 + range_start = 0 + range_end = 20 + +class KeybladeMaxStrength(Range): + """ + Determines the maximum STR bonus a keyblade can have. + """ + display_name = "Keyblade Maximum STR Bonus" + default = 14 + range_start = 0 + range_end = 20 + +class KeybladeMinMP(Range): + """ + Determines the minimum MP bonus a keyblade can have. + """ + display_name = "Keyblade Minimum MP Bonus" + default = -2 + range_start = -2 + range_end = 5 + +class KeybladeMaxMP(Range): + """ + Determines the maximum MP bonus a keyblade can have. + """ + display_name = "Keyblade Maximum MP Bonus" + default = 3 + range_start = -2 + range_end = 5 + +class LevelChecks(Range): + """ + Determines the maximum level for which checks can be obtained. + """ + display_name = "Level Checks" + default = 100 + range_start = 0 + range_end = 100 + +class ForceStatsOnLevels(NamedRange): + """ + If this value is less than the value for Level Checks, this determines the minimum level from which only stat ups are obtained at level up locations. + For example, if you want to be able to find any multiworld item from levels 1-50, then just stat ups for levels 51-100, set this value to 51. + """ + display_name = "Force Stats on Level Starting From" + default = 1 + range_start = 1 + range_end = 101 + special_range_names = { + "none": 101, + "multiworld-to-level-50": 51, + "all": 1 + } + +class BadStartingWeapons(Toggle): + """ + Forces Kingdom Key, Dream Sword, Dream Shield, and Dream Staff to have bad stats. + """ + display_name = "Bad Starting Weapons" + +class DonaldDeathLink(Toggle): + """ + If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone. + """ + display_name = "Donald Death Link" + +class GoofyDeathLink(Toggle): + """ + If Goofy is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone. + """ + display_name = "Goofy Death Link" + +class KeybladesUnlockChests(Toggle): + """ + If toggled on, the player is required to have a certain keyblade to open chests in certain worlds. + TT - Lionheart + WL - Lady Luck + OC - Olympia + DJ - Jungle King + AG - Three Wishes + MS - Wishing Star + HT - Pumpkinhead + NL - Fairy Harp + HB - Divine Rose + EotW - Oblivion + HAW - Oathkeeper + + Note: Does not apply to Atlantica, the emblem and carousel chests in Hollow Bastion, or the Aero chest in Neverland currently. + """ + display_name = "Keyblades Unlock Chests" + +class InteractInBattle(Toggle): + """ + Allow Sora to talk to people, examine objects, and open chests in battle. + """ + display_name = "Interact in Battle" + +class AdvancedLogic(Toggle): + """ + If on, logic may expect you to do advanced skips like using Combo Master, Dumbo, and other unusual methods to reach locations. + """ + display_name = "Advanced Logic" + +class ExtraSharedAbilities(Toggle): + """ + If on, adds extra shared abilities to the pool. These can stack, so multiple high jumps make you jump higher and multiple glides make you superglide faster. + """ + display_name = "Extra Shared Abilities" + +class EXPZeroInPool(Toggle): + """ + If on, adds EXP Zero ability to the item pool. This is redundant if you are planning on playing on Proud. + """ + display_name = "EXP Zero in Pool" + +class VanillaEmblemPieces(DefaultOnToggle): + """ + If on, the Hollow Bastion emblem pieces are in their vanilla locations. + """ + display_name = "Vanilla Emblem Pieces" + +class StartingWorlds(Range): + """ + Number of random worlds to start with in addition to Traverse Town, which is always available. Will only consider Atlantica if toggled, and will only consider End of the World if its unlock is set to "Item". + """ + display_name = "Starting Worlds" + default = 0 + range_start = 0 + range_end = 10 + +@dataclass +class KH1Options(PerGameCommonOptions): + goal: Goal + end_of_the_world_unlock: EndoftheWorldUnlock + final_rest_door: FinalRestDoor + required_reports_eotw: RequiredReportsEotW + required_reports_door: RequiredReportsDoor + reports_in_pool: ReportsInPool + super_bosses: SuperBosses + atlantica: Atlantica + hundred_acre_wood: HundredAcreWood + cups: Cups + puppies: Puppies + starting_worlds: StartingWorlds + keyblades_unlock_chests: KeybladesUnlockChests + interact_in_battle: InteractInBattle + exp_multiplier: EXPMultiplier + advanced_logic: AdvancedLogic + extra_shared_abilities: ExtraSharedAbilities + exp_zero_in_pool: EXPZeroInPool + vanilla_emblem_pieces: VanillaEmblemPieces + donald_death_link: DonaldDeathLink + goofy_death_link: GoofyDeathLink + randomize_keyblade_stats: RandomizeKeybladeStats + bad_starting_weapons: BadStartingWeapons + keyblade_min_str: KeybladeMinStrength + keyblade_max_str: KeybladeMaxStrength + keyblade_min_mp: KeybladeMinMP + keyblade_max_mp: KeybladeMaxMP + level_checks: LevelChecks + force_stats_on_levels: ForceStatsOnLevels + strength_increase: StrengthIncrease + defense_increase: DefenseIncrease + hp_increase: HPIncrease + ap_increase: APIncrease + mp_increase: MPIncrease + accessory_slot_increase: AccessorySlotIncrease + item_slot_increase: ItemSlotIncrease + start_inventory_from_pool: StartInventoryPool + +kh1_option_groups = [ + OptionGroup("Goal", [ + Goal, + EndoftheWorldUnlock, + FinalRestDoor, + RequiredReportsDoor, + RequiredReportsEotW, + ReportsInPool, + ]), + OptionGroup("Locations", [ + SuperBosses, + Atlantica, + Cups, + HundredAcreWood, + VanillaEmblemPieces, + ]), + OptionGroup("Levels", [ + EXPMultiplier, + LevelChecks, + ForceStatsOnLevels, + StrengthIncrease, + DefenseIncrease, + HPIncrease, + APIncrease, + MPIncrease, + AccessorySlotIncrease, + ItemSlotIncrease, + ]), + OptionGroup("Keyblades", [ + KeybladesUnlockChests, + RandomizeKeybladeStats, + BadStartingWeapons, + KeybladeMaxStrength, + KeybladeMinStrength, + KeybladeMaxMP, + KeybladeMinMP, + ]), + OptionGroup("Misc", [ + StartingWorlds, + Puppies, + InteractInBattle, + AdvancedLogic, + ExtraSharedAbilities, + EXPZeroInPool, + DonaldDeathLink, + GoofyDeathLink, + ]) +] diff --git a/worlds/kh1/Presets.py b/worlds/kh1/Presets.py new file mode 100644 index 000000000000..77b43b7624b9 --- /dev/null +++ b/worlds/kh1/Presets.py @@ -0,0 +1,177 @@ +from typing import Any, Dict + +from .Options import * + +kh1_option_presets: Dict[str, Dict[str, Any]] = { + # Standard playthrough where your goal is to defeat Ansem, reaching him by acquiring enough reports. + "Final Ansem": { + "goal": Goal.option_final_ansem, + "end_of_the_world_unlock": EndoftheWorldUnlock.option_reports, + "final_rest_door": FinalRestDoor.option_reports, + "required_reports_eotw": 7, + "required_reports_door": 10, + "reports_in_pool": 13, + + "super_bosses": False, + "atlantica": False, + "hundred_acre_wood": False, + "cups": False, + "vanilla_emblem_pieces": True, + + "exp_multiplier": 48, + "level_checks": 100, + "force_stats_on_levels": 1, + "strength_increase": 24, + "defense_increase": 24, + "hp_increase": 23, + "ap_increase": 18, + "mp_increase": 7, + "accessory_slot_increase": 1, + "item_slot_increase": 3, + + "keyblades_unlock_chests": False, + "randomize_keyblade_stats": True, + "bad_starting_weapons": False, + "keyblade_max_str": 14, + "keyblade_min_str": 3, + "keyblade_max_mp": 3, + "keyblade_min_mp": -2, + + "puppies": Puppies.option_triplets, + "starting_worlds": 0, + "interact_in_battle": False, + "advanced_logic": False, + "extra_shared_abilities": False, + "exp_zero_in_pool": False, + "donald_death_link": False, + "goofy_death_link": False + }, + # Puppies are found individually, and the goal is to return them all. + "Puppy Hunt": { + "goal": Goal.option_puppies, + "end_of_the_world_unlock": EndoftheWorldUnlock.option_item, + "final_rest_door": FinalRestDoor.option_puppies, + "required_reports_eotw": 13, + "required_reports_door": 13, + "reports_in_pool": 13, + + "super_bosses": False, + "atlantica": False, + "hundred_acre_wood": False, + "cups": False, + "vanilla_emblem_pieces": True, + + "exp_multiplier": 48, + "level_checks": 100, + "force_stats_on_levels": 1, + "strength_increase": 24, + "defense_increase": 24, + "hp_increase": 23, + "ap_increase": 18, + "mp_increase": 7, + "accessory_slot_increase": 1, + "item_slot_increase": 3, + + "keyblades_unlock_chests": False, + "randomize_keyblade_stats": True, + "bad_starting_weapons": False, + "keyblade_max_str": 14, + "keyblade_min_str": 3, + "keyblade_max_mp": 3, + "keyblade_min_mp": -2, + + "puppies": Puppies.option_individual, + "starting_worlds": 0, + "interact_in_battle": False, + "advanced_logic": False, + "extra_shared_abilities": False, + "exp_zero_in_pool": False, + "donald_death_link": False, + "goofy_death_link": False + }, + # Advanced playthrough with most settings on. + "Advanced": { + "goal": Goal.option_final_ansem, + "end_of_the_world_unlock": EndoftheWorldUnlock.option_reports, + "final_rest_door": FinalRestDoor.option_reports, + "required_reports_eotw": 7, + "required_reports_door": 10, + "reports_in_pool": 13, + + "super_bosses": True, + "atlantica": True, + "hundred_acre_wood": True, + "cups": True, + "vanilla_emblem_pieces": False, + + "exp_multiplier": 48, + "level_checks": 100, + "force_stats_on_levels": 1, + "strength_increase": 24, + "defense_increase": 24, + "hp_increase": 23, + "ap_increase": 18, + "mp_increase": 7, + "accessory_slot_increase": 1, + "item_slot_increase": 3, + + "keyblades_unlock_chests": True, + "randomize_keyblade_stats": True, + "bad_starting_weapons": True, + "keyblade_max_str": 14, + "keyblade_min_str": 3, + "keyblade_max_mp": 3, + "keyblade_min_mp": -2, + + "puppies": Puppies.option_triplets, + "starting_worlds": 0, + "interact_in_battle": True, + "advanced_logic": True, + "extra_shared_abilities": True, + "exp_zero_in_pool": True, + "donald_death_link": False, + "goofy_death_link": False + }, + # Playthrough meant to enhance the level 1 experience. + "Level 1": { + "goal": Goal.option_final_ansem, + "end_of_the_world_unlock": EndoftheWorldUnlock.option_reports, + "final_rest_door": FinalRestDoor.option_reports, + "required_reports_eotw": 7, + "required_reports_door": 10, + "reports_in_pool": 13, + + "super_bosses": False, + "atlantica": False, + "hundred_acre_wood": False, + "cups": False, + "vanilla_emblem_pieces": True, + + "exp_multiplier": 16, + "level_checks": 0, + "force_stats_on_levels": 101, + "strength_increase": 0, + "defense_increase": 0, + "hp_increase": 0, + "mp_increase": 0, + "accessory_slot_increase": 6, + "item_slot_increase": 5, + + "keyblades_unlock_chests": False, + "randomize_keyblade_stats": True, + "bad_starting_weapons": False, + "keyblade_max_str": 14, + "keyblade_min_str": 3, + "keyblade_max_mp": 3, + "keyblade_min_mp": -2, + + "puppies": Puppies.option_triplets, + "starting_worlds": 0, + "interact_in_battle": False, + "advanced_logic": False, + "extra_shared_abilities": False, + "exp_zero_in_pool": False, + "donald_death_link": False, + "goofy_death_link": False + } +} diff --git a/worlds/kh1/Regions.py b/worlds/kh1/Regions.py new file mode 100644 index 000000000000..a6f85fe617cb --- /dev/null +++ b/worlds/kh1/Regions.py @@ -0,0 +1,516 @@ +from typing import Dict, List, NamedTuple, Optional + +from BaseClasses import MultiWorld, Region, Entrance +from .Locations import KH1Location, location_table + + +class KH1RegionData(NamedTuple): + locations: List[str] + region_exits: Optional[List[str]] + + +def create_regions(multiworld: MultiWorld, player: int, options): + regions: Dict[str, KH1RegionData] = { + "Menu": KH1RegionData([], ["Awakening", "Levels"]), + "Awakening": KH1RegionData([], ["Destiny Islands"]), + "Destiny Islands": KH1RegionData([], ["Traverse Town"]), + "Traverse Town": KH1RegionData([], ["World Map"]), + "Wonderland": KH1RegionData([], []), + "Olympus Coliseum": KH1RegionData([], []), + "Deep Jungle": KH1RegionData([], []), + "Agrabah": KH1RegionData([], []), + "Monstro": KH1RegionData([], []), + "Atlantica": KH1RegionData([], []), + "Halloween Town": KH1RegionData([], []), + "Neverland": KH1RegionData([], []), + "Hollow Bastion": KH1RegionData([], []), + "End of the World": KH1RegionData([], []), + "100 Acre Wood": KH1RegionData([], []), + "Levels": KH1RegionData([], []), + "World Map": KH1RegionData([], ["Wonderland", "Olympus Coliseum", "Deep Jungle", + "Agrabah", "Monstro", "Atlantica", + "Halloween Town", "Neverland", "Hollow Bastion", + "End of the World", "100 Acre Wood"]) + } + + # Set up locations + regions["Agrabah"].locations.append("Agrabah Aladdin's House Main Street Entrance Chest") + regions["Agrabah"].locations.append("Agrabah Aladdin's House Plaza Entrance Chest") + regions["Agrabah"].locations.append("Agrabah Alley Chest") + regions["Agrabah"].locations.append("Agrabah Bazaar Across Windows Chest") + regions["Agrabah"].locations.append("Agrabah Bazaar High Corner Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Pillar Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Raised Platform Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Abu Gem Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Across from Relic Chamber Entrance Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Bridge Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Near Save Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance Left Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance Tall Tower Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance White Trinity Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hall High Left Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hall Near Bottomless Hall Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hidden Room Left Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hidden Room Right Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Relic Chamber Jump from Stairs Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Relic Chamber Stairs Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Above Fire Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Across Platforms Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest") + regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest") + regions["Agrabah"].locations.append("Agrabah Defeat Jafar Blizzard Event") + regions["Agrabah"].locations.append("Agrabah Defeat Jafar Genie Ansem's Report 1") + regions["Agrabah"].locations.append("Agrabah Defeat Jafar Genie Fire Event") + regions["Agrabah"].locations.append("Agrabah Defeat Pot Centipede Ray of Light Event") + regions["Agrabah"].locations.append("Agrabah Main Street High Above Alley Entrance Chest") + regions["Agrabah"].locations.append("Agrabah Main Street High Above Palace Gates Entrance Chest") + regions["Agrabah"].locations.append("Agrabah Main Street Right Palace Entrance Chest") + regions["Agrabah"].locations.append("Agrabah Palace Gates High Close to Palace Chest") + regions["Agrabah"].locations.append("Agrabah Palace Gates High Opposite Palace Chest") + regions["Agrabah"].locations.append("Agrabah Palace Gates Low Chest") + regions["Agrabah"].locations.append("Agrabah Plaza By Storage Chest") + regions["Agrabah"].locations.append("Agrabah Plaza Raised Terrace Chest") + regions["Agrabah"].locations.append("Agrabah Plaza Top Corner Chest") + regions["Agrabah"].locations.append("Agrabah Seal Keyhole Genie Event") + regions["Agrabah"].locations.append("Agrabah Seal Keyhole Green Trinity Event") + regions["Agrabah"].locations.append("Agrabah Seal Keyhole Three Wishes Event") + regions["Agrabah"].locations.append("Agrabah Storage Behind Barrel Chest") + regions["Agrabah"].locations.append("Agrabah Storage Green Trinity Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Bamboo Thicket Save Gorillas") + regions["Deep Jungle"].locations.append("Deep Jungle Camp Blue Trinity Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Camp Ether Experiment") + regions["Deep Jungle"].locations.append("Deep Jungle Camp Hi-Potion Experiment") + regions["Deep Jungle"].locations.append("Deep Jungle Camp Replication Experiment") + regions["Deep Jungle"].locations.append("Deep Jungle Camp Save Gorillas") + regions["Deep Jungle"].locations.append("Deep Jungle Cavern of Hearts Navi-G Piece Event") + regions["Deep Jungle"].locations.append("Deep Jungle Cavern of Hearts White Trinity Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Cliff Right Cliff Left Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Cliff Right Cliff Right Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Cliff Save Gorillas") + regions["Deep Jungle"].locations.append("Deep Jungle Climbing Trees Blue Trinity Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Climbing Trees Save Gorillas") + regions["Deep Jungle"].locations.append("Deep Jungle Defeat Clayton Cure Event") + regions["Deep Jungle"].locations.append("Deep Jungle Defeat Sabor White Fang Event") + regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Center Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Left Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Right Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 10 Fruits") + regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 20 Fruits") + regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 30 Fruits") + regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 40 Fruits") + regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 50 Fruits") + regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Jungle King Event") + regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Red Trinity Event") + regions["Deep Jungle"].locations.append("Deep Jungle Tent Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Tent Protect-G Event") + regions["Deep Jungle"].locations.append("Deep Jungle Tree House Beneath Tree House Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Tree House Rooftop Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Tree House Save Gorillas") + regions["Deep Jungle"].locations.append("Deep Jungle Tree House Suspended Boat Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Tunnel Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Vines 2 Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Vines Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern High Middle Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern High Wall Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern Low Chest") + regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern Middle Chest") + regions["End of the World"].locations.append("End of the World Defeat Chernabog Superglide Event") + regions["End of the World"].locations.append("End of the World Final Dimension 10th Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 1st Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 2nd Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 3rd Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 4th Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 5th Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 6th Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 7th Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 8th Chest") + regions["End of the World"].locations.append("End of the World Final Dimension 9th Chest") + regions["End of the World"].locations.append("End of the World Final Rest Chest") + regions["End of the World"].locations.append("End of the World Giant Crevasse 1st Chest") + regions["End of the World"].locations.append("End of the World Giant Crevasse 2nd Chest") + regions["End of the World"].locations.append("End of the World Giant Crevasse 3rd Chest") + regions["End of the World"].locations.append("End of the World Giant Crevasse 4th Chest") + regions["End of the World"].locations.append("End of the World Giant Crevasse 5th Chest") + regions["End of the World"].locations.append("End of the World World Terminus 100 Acre Wood Chest") + regions["End of the World"].locations.append("End of the World World Terminus Agrabah Chest") + regions["End of the World"].locations.append("End of the World World Terminus Atlantica Chest") + regions["End of the World"].locations.append("End of the World World Terminus Deep Jungle Chest") + regions["End of the World"].locations.append("End of the World World Terminus Halloween Town Chest") + #regions["End of the World"].locations.append("End of the World World Terminus Hollow Bastion Chest") + regions["End of the World"].locations.append("End of the World World Terminus Neverland Chest") + regions["End of the World"].locations.append("End of the World World Terminus Olympus Coliseum Chest") + regions["End of the World"].locations.append("End of the World World Terminus Traverse Town Chest") + regions["End of the World"].locations.append("End of the World World Terminus Wonderland Chest") + regions["Halloween Town"].locations.append("Halloween Town Boneyard Tombstone Puzzle Chest") + regions["Halloween Town"].locations.append("Halloween Town Bridge Left of Gate Chest") + regions["Halloween Town"].locations.append("Halloween Town Bridge Right of Gate Chest") + regions["Halloween Town"].locations.append("Halloween Town Bridge Under Bridge") + regions["Halloween Town"].locations.append("Halloween Town Cemetery Behind Grave Chest") + regions["Halloween Town"].locations.append("Halloween Town Cemetery Between Graves Chest") + regions["Halloween Town"].locations.append("Halloween Town Cemetery By Cat Shape Chest") + regions["Halloween Town"].locations.append("Halloween Town Cemetery By Striped Grave Chest") + regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie Boogie Ansem's Report 7") + regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie Boogie Holy Circlet Event") + regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie's Manor Gravity Event") + regions["Halloween Town"].locations.append("Halloween Town Graveyard Forget-Me-Not Event") + regions["Halloween Town"].locations.append("Halloween Town Guillotine Square High Tower Chest") + regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Pumpkin Structure Left Chest") + regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Pumpkin Structure Right Chest") + regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Ring Jack's Doorbell 3 Times") + regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Under Jack's House Stairs Chest") + regions["Halloween Town"].locations.append("Halloween Town Lab Torn Page") + regions["Halloween Town"].locations.append("Halloween Town Moonlight Hill White Trinity Chest") + regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Entrance Steps Chest") + regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Grounds Red Trinity Chest") + regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Hollow Chest") + regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Inside Entrance Chest") + regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Lower Iron Cage Chest") + regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Upper Iron Cage Chest") + regions["Halloween Town"].locations.append("Halloween Town Seal Keyhole Pumpkinhead Event") + regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Bubble Under the Wall Platform Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Near Crystal Switch Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Platform Near Entrance Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates Freestanding Pillar Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates Gravity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates High Pillar Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Behemoth Omega Arts Event") + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Dragon Maleficent Fireglow Event") + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Maleficent Ansem's Report 5") + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Maleficent Donald Cheer Event") + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku I White Trinity Event") + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku II Ragnarok Event") + regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon By Candles Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon Corner Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Chest)") + regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Flame)") + regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Fountain)") + regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Statue)") + regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Left of Emblem Door Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Left of Gate Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Oblivion Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Steps Right Side Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest After Battle Platform Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest Lower Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 1st Gravity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 2nd Gravity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower Above Sliding Blocks Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Library 1st Floor Turn the Carousel Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Library 2nd Floor Turn the Carousel 1st Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Library 2nd Floor Turn the Carousel 2nd Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Library Speak to Aerith Cure") + regions["Hollow Bastion"].locations.append("Hollow Bastion Library Speak to Belle Divine Rose") + regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Turn the Carousel Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node Gravity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Outside Library Gravity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Floating Platform Near Bubble Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Floating Platform Near Save Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls High Platform Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Under Water 1st Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Under Water 2nd Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Water's Surface Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls White Trinity Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Speak to Princesses Fire Event") + regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 10") + regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 2") + regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 4") + regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 6") + regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Blizzard on Bubble Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Near Save Chest") + regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Unlock Passage from Base Level Chest") + regions["Monstro"].locations.append("Monstro Chamber 2 Ground Chest") + regions["Monstro"].locations.append("Monstro Chamber 2 Platform Chest") + regions["Monstro"].locations.append("Monstro Chamber 3 Ground Chest") + regions["Monstro"].locations.append("Monstro Chamber 3 Near Chamber 6 Entrance Chest") + regions["Monstro"].locations.append("Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest") + regions["Monstro"].locations.append("Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest") + regions["Monstro"].locations.append("Monstro Chamber 5 Atop Barrel Chest") + regions["Monstro"].locations.append("Monstro Chamber 5 Low 1st Chest") + regions["Monstro"].locations.append("Monstro Chamber 5 Low 2nd Chest") + regions["Monstro"].locations.append("Monstro Chamber 5 Platform Chest") + regions["Monstro"].locations.append("Monstro Chamber 6 Low Chest") + regions["Monstro"].locations.append("Monstro Chamber 6 Other Platform Chest") + regions["Monstro"].locations.append("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest") + regions["Monstro"].locations.append("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest") + regions["Monstro"].locations.append("Monstro Chamber 6 White Trinity Chest") + regions["Monstro"].locations.append("Monstro Defeat Parasite Cage I Goofy Cheer Event") + regions["Monstro"].locations.append("Monstro Defeat Parasite Cage II Stop Event") + regions["Monstro"].locations.append("Monstro Mouth Boat Deck Chest") + regions["Monstro"].locations.append("Monstro Mouth Green Trinity Top of Boat Chest") + regions["Monstro"].locations.append("Monstro Mouth High Platform Across from Boat Chest") + regions["Monstro"].locations.append("Monstro Mouth High Platform Boat Side Chest") + regions["Monstro"].locations.append("Monstro Mouth High Platform Near Teeth Chest") + regions["Monstro"].locations.append("Monstro Mouth Near Ship Chest") + regions["Neverland"].locations.append("Neverland Cabin Chest") + regions["Neverland"].locations.append("Neverland Captain's Cabin Chest") + #regions["Neverland"].locations.append("Neverland Clock Tower 01:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 02:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 03:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 04:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 05:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 06:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 07:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 08:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 09:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 10:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 11:00 Door") + #regions["Neverland"].locations.append("Neverland Clock Tower 12:00 Door") + regions["Neverland"].locations.append("Neverland Clock Tower Chest") + regions["Neverland"].locations.append("Neverland Defeat Anti Sora Raven's Claw Event") + regions["Neverland"].locations.append("Neverland Defeat Captain Hook Ars Arcanum Event") + regions["Neverland"].locations.append("Neverland Defeat Hook Ansem's Report 9") + regions["Neverland"].locations.append("Neverland Encounter Hook Cure Event") + regions["Neverland"].locations.append("Neverland Galley Chest") + regions["Neverland"].locations.append("Neverland Hold Aero Chest") + regions["Neverland"].locations.append("Neverland Hold Flight 1st Chest") + regions["Neverland"].locations.append("Neverland Hold Flight 2nd Chest") + regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Green Chest") + regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Left Blue Chest") + regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Right Blue Chest") + regions["Neverland"].locations.append("Neverland Pirate Ship Crows Nest Chest") + regions["Neverland"].locations.append("Neverland Pirate Ship Deck White Trinity Chest") + regions["Neverland"].locations.append("Neverland Seal Keyhole Fairy Harp Event") + regions["Neverland"].locations.append("Neverland Seal Keyhole Glide Event") + regions["Neverland"].locations.append("Neverland Seal Keyhole Tinker Bell Event") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Clear Phil's Training Thunder Event") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Cloud Sonic Blade Event") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Blizzaga Chest") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Blizzara Chest") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Entry Pass Event") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Green Trinity") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Hero's License Event") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Left Behind Columns Chest") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Left Blue Trinity Chest") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Right Blue Trinity Chest") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates White Trinity Chest") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Cerberus Inferno Band Event") + regions["Traverse Town"].locations.append("Traverse Town 1st District Accessory Shop Roof Chest") + #regions["Traverse Town"].locations.append("Traverse Town 1st District Aerith Gift") + regions["Traverse Town"].locations.append("Traverse Town 1st District Blue Trinity Balcony Chest") + regions["Traverse Town"].locations.append("Traverse Town 1st District Candle Puzzle Chest") + #regions["Traverse Town"].locations.append("Traverse Town 1st District Leon Gift") + regions["Traverse Town"].locations.append("Traverse Town 1st District Safe Postcard") + regions["Traverse Town"].locations.append("Traverse Town 1st District Speak with Cid Event") + regions["Traverse Town"].locations.append("Traverse Town 2nd District Boots and Shoes Awning Chest") + regions["Traverse Town"].locations.append("Traverse Town 2nd District Gizmo Shop Facade Chest") + regions["Traverse Town"].locations.append("Traverse Town 2nd District Rooftop Chest") + regions["Traverse Town"].locations.append("Traverse Town 3rd District Balcony Postcard") + regions["Traverse Town"].locations.append("Traverse Town Accessory Shop Chest") + regions["Traverse Town"].locations.append("Traverse Town Alleyway Balcony Chest") + regions["Traverse Town"].locations.append("Traverse Town Alleyway Behind Crates Chest") + regions["Traverse Town"].locations.append("Traverse Town Alleyway Blue Room Awning Chest") + regions["Traverse Town"].locations.append("Traverse Town Alleyway Corner Chest") + regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Blue Trinity Event") + regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Brave Warrior Event") + regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Dodge Roll Event") + regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Fire Event") + regions["Traverse Town"].locations.append("Traverse Town Defeat Opposite Armor Aero Event") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Chest") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto All Summons Reward") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 1") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 2") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 3") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 4") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 5") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Postcard") + regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Talk to Pinocchio") + regions["Traverse Town"].locations.append("Traverse Town Gizmo Shop Postcard 1") + regions["Traverse Town"].locations.append("Traverse Town Gizmo Shop Postcard 2") + regions["Traverse Town"].locations.append("Traverse Town Green Room Clock Puzzle Chest") + regions["Traverse Town"].locations.append("Traverse Town Green Room Table Chest") + regions["Traverse Town"].locations.append("Traverse Town Item Shop Postcard") + regions["Traverse Town"].locations.append("Traverse Town Item Workshop Left Chest") + regions["Traverse Town"].locations.append("Traverse Town Item Workshop Postcard") + regions["Traverse Town"].locations.append("Traverse Town Item Workshop Right Chest") + regions["Traverse Town"].locations.append("Traverse Town Kairi Secret Waterway Oathkeeper Event") + regions["Traverse Town"].locations.append("Traverse Town Leon Secret Waterway Earthshine Event") + regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All Arts Items") + regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV1 Magic") + regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV3 Magic") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 01 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 02 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 03 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 04 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 05 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 06 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 07 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 08 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 09 Event") + regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 10 Event") + regions["Traverse Town"].locations.append("Traverse Town Mystical House Glide Chest") + regions["Traverse Town"].locations.append("Traverse Town Mystical House Yellow Trinity Chest") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 10 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 20 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 30 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 40 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 50 Puppies Reward 1") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 50 Puppies Reward 2") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 60 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 70 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 80 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 90 Puppies") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 1") + regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 2") + regions["Traverse Town"].locations.append("Traverse Town Red Room Chest") + regions["Traverse Town"].locations.append("Traverse Town Secret Waterway Near Stairs Chest") + regions["Traverse Town"].locations.append("Traverse Town Secret Waterway White Trinity Chest") + regions["Traverse Town"].locations.append("Traverse Town Synth Cloth") + regions["Traverse Town"].locations.append("Traverse Town Synth Fish") + regions["Traverse Town"].locations.append("Traverse Town Synth Log") + regions["Traverse Town"].locations.append("Traverse Town Synth Mushroom") + regions["Traverse Town"].locations.append("Traverse Town Synth Rope") + regions["Traverse Town"].locations.append("Traverse Town Synth Seagull Egg") + regions["Wonderland"].locations.append("Wonderland Bizarre Room Green Trinity Chest") + regions["Wonderland"].locations.append("Wonderland Bizarre Room Lamp Chest") + regions["Wonderland"].locations.append("Wonderland Bizarre Room Navi-G Piece Event") + regions["Wonderland"].locations.append("Wonderland Bizarre Room Read Book") + regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Blizzard Event") + regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Ifrit's Horn Event") + regions["Wonderland"].locations.append("Wonderland Lotus Forest Corner Chest") + regions["Wonderland"].locations.append("Wonderland Lotus Forest Glide Chest") + regions["Wonderland"].locations.append("Wonderland Lotus Forest Nut Chest") + regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting Thunder Plant Chest") + regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting White Trinity Chest") + regions["Wonderland"].locations.append("Wonderland Lotus Forest Thunder Plant Chest") + regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Left Red Chest") + regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Blue Chest") + regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Red Chest") + regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 1 Chest") + regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 2 Chest") + regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 3 Chest") + regions["Wonderland"].locations.append("Wonderland Rabbit Hole Green Trinity Chest") + regions["Wonderland"].locations.append("Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest") + regions["Wonderland"].locations.append("Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest") + regions["Wonderland"].locations.append("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest") + regions["Wonderland"].locations.append("Wonderland Tea Party Garden Bear and Clock Puzzle Chest") + if options.hundred_acre_wood: + regions["100 Acre Wood"].locations.append("100 Acre Wood Meadow Inside Log Chest") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Left Cliff Chest") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Right Tree Alcove Chest") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Under Giant Pot Chest") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 1") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 2") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 3") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 4") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 5") + regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's House Owl Cheer") + regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 1") + regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 2") + regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 3") + regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 4") + regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 5") + regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's House Start Fire") + regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's Room Cabinet") + regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's Room Chimney") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Break Log") + regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Fall Through Top of Tree Next to Pooh") + if options.atlantica: + regions["Atlantica"].locations.append("Atlantica Sunken Ship In Flipped Boat Chest") + regions["Atlantica"].locations.append("Atlantica Sunken Ship Seabed Chest") + regions["Atlantica"].locations.append("Atlantica Sunken Ship Inside Ship Chest") + regions["Atlantica"].locations.append("Atlantica Ariel's Grotto High Chest") + regions["Atlantica"].locations.append("Atlantica Ariel's Grotto Middle Chest") + regions["Atlantica"].locations.append("Atlantica Ariel's Grotto Low Chest") + regions["Atlantica"].locations.append("Atlantica Ursula's Lair Use Fire on Urchin Chest") + regions["Atlantica"].locations.append("Atlantica Undersea Gorge Jammed by Ariel's Grotto Chest") + regions["Atlantica"].locations.append("Atlantica Triton's Palace White Trinity Chest") + regions["Atlantica"].locations.append("Atlantica Defeat Ursula I Mermaid Kick Event") + regions["Atlantica"].locations.append("Atlantica Defeat Ursula II Thunder Event") + regions["Atlantica"].locations.append("Atlantica Seal Keyhole Crabclaw Event") + regions["Atlantica"].locations.append("Atlantica Undersea Gorge Blizzard Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Gorge Ocean Floor Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Valley Higher Cave Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Valley Lower Cave Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Valley Fire Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Valley Wall Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Valley Pillar Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Valley Ocean Floor Clam") + regions["Atlantica"].locations.append("Atlantica Triton's Palace Thunder Clam") + regions["Atlantica"].locations.append("Atlantica Triton's Palace Wall Right Clam") + regions["Atlantica"].locations.append("Atlantica Triton's Palace Near Path Clam") + regions["Atlantica"].locations.append("Atlantica Triton's Palace Wall Left Clam") + regions["Atlantica"].locations.append("Atlantica Cavern Nook Clam") + regions["Atlantica"].locations.append("Atlantica Below Deck Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Garden Clam") + regions["Atlantica"].locations.append("Atlantica Undersea Cave Clam") + regions["Atlantica"].locations.append("Atlantica Sunken Ship Crystal Trident Event") + regions["Atlantica"].locations.append("Atlantica Defeat Ursula II Ansem's Report 3") + if options.cups: + regions["Olympus Coliseum"].locations.append("Complete Phil Cup") + regions["Olympus Coliseum"].locations.append("Complete Phil Cup Solo") + regions["Olympus Coliseum"].locations.append("Complete Phil Cup Time Trial") + regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup") + regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup Solo") + regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup Time Trial") + regions["Olympus Coliseum"].locations.append("Complete Hercules Cup") + regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Solo") + regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Time Trial") + regions["Olympus Coliseum"].locations.append("Complete Hades Cup") + regions["Olympus Coliseum"].locations.append("Complete Hades Cup Solo") + regions["Olympus Coliseum"].locations.append("Complete Hades Cup Time Trial") + regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cloud and Leon Event") + regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Yuffie Event") + regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cerberus Event") + regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Behemoth Event") + regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Hades Event") + regions["Olympus Coliseum"].locations.append("Hercules Cup Defeat Cloud Event") + regions["Olympus Coliseum"].locations.append("Hercules Cup Yellow Trinity Event") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Hades Ansem's Report 8") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Olympia Chest") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Ice Titan Diamond Dust Event") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Gates Purple Jar After Defeating Hades") + if options.super_bosses: + regions["Neverland"].locations.append("Neverland Defeat Phantom Stop Event") + regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Zantetsuken Event") + regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Ansem's Report 11") + if options.super_bosses or options.goal.current_key == "sephiroth": + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth Ansem's Report 12") + regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth One-Winged Angel Event") + if options.super_bosses or options.goal.current_key == "unknown": + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown Ansem's Report 13") + regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown EXP Necklace Event") + for i in range(options.level_checks): + regions["Levels"].locations.append("Level " + str(i+1).rjust(3, '0')) + if options.goal.current_key == "final_ansem": + regions["End of the World"].locations.append("Final Ansem") + + # Set up the regions correctly. + for name, data in regions.items(): + multiworld.regions.append(create_region(multiworld, player, name, data)) + + multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player)) + multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player)) + multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player)) + multiworld.get_entrance("Wonderland", player).connect(multiworld.get_region("Wonderland", player)) + multiworld.get_entrance("Olympus Coliseum", player).connect(multiworld.get_region("Olympus Coliseum", player)) + multiworld.get_entrance("Deep Jungle", player).connect(multiworld.get_region("Deep Jungle", player)) + multiworld.get_entrance("Agrabah", player).connect(multiworld.get_region("Agrabah", player)) + multiworld.get_entrance("Monstro", player).connect(multiworld.get_region("Monstro", player)) + multiworld.get_entrance("Atlantica", player).connect(multiworld.get_region("Atlantica", player)) + multiworld.get_entrance("Halloween Town", player).connect(multiworld.get_region("Halloween Town", player)) + multiworld.get_entrance("Neverland", player).connect(multiworld.get_region("Neverland", player)) + multiworld.get_entrance("Hollow Bastion", player).connect(multiworld.get_region("Hollow Bastion", player)) + multiworld.get_entrance("End of the World", player).connect(multiworld.get_region("End of the World", player)) + multiworld.get_entrance("100 Acre Wood", player).connect(multiworld.get_region("100 Acre Wood", player)) + multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player)) + multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player)) + +def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData): + region = Region(name, player, multiworld) + if data.locations: + for loc_name in data.locations: + loc_data = location_table.get(loc_name) + location = KH1Location(player, loc_name, loc_data.code if loc_data else None, region) + region.locations.append(location) + + if data.region_exits: + for exit in data.region_exits: + entrance = Entrance(player, exit, region) + region.exits.append(entrance) + + return region diff --git a/worlds/kh1/Rules.py b/worlds/kh1/Rules.py new file mode 100644 index 000000000000..130238e5048e --- /dev/null +++ b/worlds/kh1/Rules.py @@ -0,0 +1,1958 @@ +from BaseClasses import CollectionState +from worlds.generic.Rules import add_rule +from math import ceil + +SINGLE_PUPPIES = ["Puppy " + str(i).rjust(2,"0") for i in range(1,100)] +TRIPLE_PUPPIES = ["Puppies " + str(3*(i-1)+1).rjust(2, "0") + "-" + str(3*(i-1)+3).rjust(2, "0") for i in range(1,34)] +TORN_PAGES = ["Torn Page " + str(i) for i in range(1,6)] +WORLDS = ["Wonderland", "Olympus Coliseum", "Deep Jungle", "Agrabah", "Monstro", "Atlantica", "Halloween Town", "Neverland", "Hollow Bastion", "End of the World"] +KEYBLADES = ["Lady Luck", "Olympia", "Jungle King", "Three Wishes", "Wishing Star", "Crabclaw", "Pumpkinhead", "Fairy Harp", "Divine Rose", "Oblivion"] + +def has_x_worlds(state: CollectionState, player: int, num_of_worlds: int, keyblades_unlock_chests: bool) -> bool: + worlds_acquired = 0.0 + for i in range(len(WORLDS)): + if state.has(WORLDS[i], player): + worlds_acquired = worlds_acquired + 0.5 + if (state.has(WORLDS[i], player) and (not keyblades_unlock_chests or state.has(KEYBLADES[i], player))) or (state.has(WORLDS[i], player) and WORLDS[i] == "Atlantica"): + worlds_acquired = worlds_acquired + 0.5 + return worlds_acquired >= num_of_worlds + +def has_emblems(state: CollectionState, player: int, keyblades_unlock_chests: bool) -> bool: + return state.has_all({ + "Emblem Piece (Flame)", + "Emblem Piece (Chest)", + "Emblem Piece (Statue)", + "Emblem Piece (Fountain)", + "Hollow Bastion"}, player) and has_x_worlds(state, player, 5, keyblades_unlock_chests) + +def has_puppies_all(state: CollectionState, player: int, puppies_required: int) -> bool: + return state.has("All Puppies", player) + +def has_puppies_triplets(state: CollectionState, player: int, puppies_required: int) -> bool: + return state.has_from_list_unique(TRIPLE_PUPPIES, player, ceil(puppies_required / 3)) + +def has_puppies_individual(state: CollectionState, player: int, puppies_required: int) -> bool: + return state.has_from_list_unique(SINGLE_PUPPIES, player, puppies_required) + +def has_torn_pages(state: CollectionState, player: int, pages_required: int) -> bool: + return state.count_from_list_unique(TORN_PAGES, player) >= pages_required + +def has_all_arts(state: CollectionState, player: int) -> bool: + return state.has_all({"Fire Arts", "Blizzard Arts", "Thunder Arts", "Cure Arts", "Gravity Arts", "Stop Arts", "Aero Arts"}, player) + +def has_all_summons(state: CollectionState, player: int) -> bool: + return state.has_all({"Simba", "Bambi", "Genie", "Dumbo", "Mushu", "Tinker Bell"}, player) + +def has_all_magic_lvx(state: CollectionState, player: int, level) -> bool: + return state.has_all_counts({ + "Progressive Fire": level, + "Progressive Blizzard": level, + "Progressive Thunder": level, + "Progressive Cure": level, + "Progressive Gravity": level, + "Progressive Aero": level, + "Progressive Stop": level}, player) + +def has_offensive_magic(state: CollectionState, player: int) -> bool: + return state.has_any({"Progressive Fire", "Progressive Blizzard", "Progressive Thunder", "Progressive Gravity", "Progressive Stop"}, player) + +def has_reports(state: CollectionState, player: int, eotw_required_reports: int) -> bool: + return state.has_group_unique("Reports", player, eotw_required_reports) + +def has_final_rest_door(state: CollectionState, player: int, final_rest_door_requirement: str, final_rest_door_required_reports: int, keyblades_unlock_chests: bool, puppies_choice: str): + if final_rest_door_requirement == "reports": + return state.has_group_unique("Reports", player, final_rest_door_required_reports) + if final_rest_door_requirement == "puppies": + if puppies_choice == "individual": + return has_puppies_individual(state, player, 99) + if puppies_choice == "triplets": + return has_puppies_triplets(state, player, 99) + return has_puppies_all(state, player, 99) + if final_rest_door_requirement == "postcards": + return state.has("Postcard", player, 10) + if final_rest_door_requirement == "superbosses": + return ( + state.has_all({"Olympus Coliseum", "Neverland", "Agrabah", "Hollow Bastion", "Green Trinity", "Phil Cup", "Pegasus Cup", "Hercules Cup", "Entry Pass"}, player) + and has_emblems(state, player, keyblades_unlock_chests) + and has_all_magic_lvx(state, player, 2) + and has_defensive_tools(state, player) + and has_x_worlds(state, player, 7, keyblades_unlock_chests) + ) + +def has_defensive_tools(state: CollectionState, player: int) -> bool: + return ( + state.has_all_counts({"Progressive Cure": 2, "Leaf Bracer": 1, "Dodge Roll": 1}, player) + and state.has_any_count({"Second Chance": 1, "MP Rage": 1, "Progressive Aero": 2}, player) + ) + +def can_dumbo_skip(state: CollectionState, player: int) -> bool: + return ( + state.has("Dumbo", player) + and state.has_group("Magic", player) + ) + +def has_oogie_manor(state: CollectionState, player: int, advanced_logic: bool) -> bool: + return ( + state.has("Progressive Fire", player) + or (advanced_logic and state.has("High Jump", player, 2)) + or (advanced_logic and state.has("High Jump", player) and state.has("Progressive Glide", player)) + ) + +def set_rules(kh1world): + multiworld = kh1world.multiworld + player = kh1world.player + options = kh1world.options + eotw_required_reports = kh1world.determine_reports_required_to_open_end_of_the_world() + final_rest_door_required_reports = kh1world.determine_reports_required_to_open_final_rest_door() + final_rest_door_requirement = kh1world.options.final_rest_door.current_key + + has_puppies = has_puppies_individual + if kh1world.options.puppies == "triplets": + has_puppies = has_puppies_triplets + elif kh1world.options.puppies == "full": + has_puppies = has_puppies_all + + add_rule(kh1world.get_location("Traverse Town 1st District Candle Puzzle Chest"), + lambda state: state.has("Progressive Blizzard", player)) + add_rule(kh1world.get_location("Traverse Town Mystical House Yellow Trinity Chest"), + lambda state: ( + state.has("Progressive Fire", player) + and + ( + state.has("Yellow Trinity", player) + or (options.advanced_logic and state.has("High Jump", player)) + or state.has("High Jump", player, 2) + ) + )) + add_rule(kh1world.get_location("Traverse Town Secret Waterway White Trinity Chest"), + lambda state: state.has("White Trinity", player)) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Chest"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Item Workshop Right Chest"), + lambda state: ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + )) + add_rule(kh1world.get_location("Traverse Town 1st District Blue Trinity Balcony Chest"), + lambda state: ( + (state.has("Blue Trinity", player) and state.has("Progressive Glide", player)) + or (options.advanced_logic and state.has("Progressive Glide", player)) + )) + add_rule(kh1world.get_location("Traverse Town Mystical House Glide Chest"), + lambda state: ( + ( + state.has("Progressive Glide", player) + or + ( + options.advanced_logic + and + ( + (state.has("High Jump", player) and state.has("Yellow Trinity", player)) + or state.has("High Jump", player, 2) + ) + and state.has("Combo Master", player) + ) + or + ( + options.advanced_logic + and state.has("Mermaid Kick", player) + ) + ) + and state.has("Progressive Fire", player) + )) + add_rule(kh1world.get_location("Traverse Town Alleyway Behind Crates Chest"), + lambda state: state.has("Red Trinity", player)) + add_rule(kh1world.get_location("Traverse Town Item Workshop Left Chest"), + lambda state: ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + )) + add_rule(kh1world.get_location("Wonderland Rabbit Hole Green Trinity Chest"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Wonderland Rabbit Hole Defeat Heartless 3 Chest"), + lambda state: has_x_worlds(state, player, 5, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Wonderland Bizarre Room Green Trinity Chest"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Wonderland Queen's Castle Hedge Left Red Chest"), + lambda state: ( + state.has("Footprints", player) + or state.has("High Jump", player) + or state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("Wonderland Queen's Castle Hedge Right Blue Chest"), + lambda state: ( + state.has("Footprints", player) + or state.has("High Jump", player) + or state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("Wonderland Queen's Castle Hedge Right Red Chest"), + lambda state: ( + state.has("Footprints", player) + or state.has("High Jump", player) + or state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("Wonderland Lotus Forest Thunder Plant Chest"), + lambda state: ( + state.has_all({ + "Progressive Thunder", + "Footprints"}, player) + )) + add_rule(kh1world.get_location("Wonderland Lotus Forest Through the Painting Thunder Plant Chest"), + lambda state: ( + state.has_all({ + "Progressive Thunder", + "Footprints"}, player) + )) + add_rule(kh1world.get_location("Wonderland Lotus Forest Glide Chest"), + lambda state: ( + state.has("Progressive Glide", player) + or + ( + options.advanced_logic + and (state.has("High Jump", player) or can_dumbo_skip(state, player)) + and state.has("Footprints", player) + ) + )) + add_rule(kh1world.get_location("Wonderland Lotus Forest Corner Chest"), + lambda state: ( + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Wonderland Bizarre Room Lamp Chest"), + lambda state: state.has("Footprints", player)) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest"), + lambda state: ( + state.has("Progressive Glide", player) + or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or + ( + options.advanced_logic + and state.has_all({ + "High Jump", + "Footprints"}, player) + ) + )) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest"), + lambda state: ( + state.has("Progressive Glide", player) + or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or + ( + options.advanced_logic + and state.has_all({ + "High Jump", + "Footprints"}, player) + ) + )) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Bear and Clock Puzzle Chest"), + lambda state: ( + + state.has("Footprints", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + )) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"), + lambda state: ( + state.has("Progressive Glide", player) + or + ( + state.has("High Jump", player, 3) + and state.has("Footprints", player) + ) + or + ( + options.advanced_logic + and state.has_all({ + "High Jump", + "Footprints", + "Combo Master"}, player) + ) + )) + add_rule(kh1world.get_location("Wonderland Lotus Forest Through the Painting White Trinity Chest"), + lambda state: ( + state.has_all({ + "White Trinity", + "Footprints"}, player) + )) + add_rule(kh1world.get_location("Deep Jungle Hippo's Lagoon Right Chest"), + lambda state: ( + + state.has("High Jump", player) + or state.has("Progressive Glide", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Deep Jungle Climbing Trees Blue Trinity Chest"), + lambda state: state.has("Blue Trinity", player)) + add_rule(kh1world.get_location("Deep Jungle Cavern of Hearts White Trinity Chest"), + lambda state: ( + state.has_all({ + "White Trinity", + "Slides"}, player) + )) + add_rule(kh1world.get_location("Deep Jungle Camp Blue Trinity Chest"), + lambda state: state.has("Blue Trinity", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern Low Chest"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern Middle Chest"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern High Wall Chest"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern High Middle Chest"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Tree House Suspended Boat Chest"), + lambda state: ( + state.has("Progressive Glide", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Agrabah Main Street High Above Palace Gates Entrance Chest"), + lambda state: ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + or (options.advanced_logic and can_dumbo_skip(state, player)) + )) + add_rule(kh1world.get_location("Agrabah Palace Gates High Opposite Palace Chest"), + lambda state: ( + state.has("High Jump", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Agrabah Palace Gates High Close to Palace Chest"), + lambda state: ( + ( + state.has_all({ + "High Jump", + "Progressive Glide"}, player) + or + ( + options.advanced_logic + and + ( + state.has("Combo Master", player) + or can_dumbo_skip(state, player) + ) + ) + ) + or state.has("High Jump", player, 3) + or (options.advanced_logic and state.has("Progressive Glide", player)) + )) + add_rule(kh1world.get_location("Agrabah Storage Green Trinity Chest"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Entrance Tall Tower Chest"), + lambda state: ( + state.has("Progressive Glide", player) + or (options.advanced_logic and state.has("Combo Master", player)) + or (options.advanced_logic and can_dumbo_skip(state, player)) + or state.has("High Jump", player, 2) + )) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Bottomless Hall Pillar Chest"), + lambda state: ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest"), + lambda state: state.has("Blue Trinity", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Hidden Room Right Chest"), + lambda state: ( + state.has("Yellow Trinity", player) + or state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + )) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Hidden Room Left Chest"), + lambda state: ( + state.has("Yellow Trinity", player) + or state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + )) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Entrance White Trinity Chest"), + lambda state: state.has("White Trinity", player)) + add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"), + lambda state: ( + state.has_all(("High Jump", "Progressive Glide"), player) + or (options.advanced_logic and state.has("Combo Master", player)) + )) + add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"), + lambda state: ( + state.has("High Jump", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"), + lambda state: ( + state.has_all(("High Jump", "Progressive Glide"), player) + or (options.advanced_logic and state.has("Combo Master", player)) + )) + add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"), + lambda state: ( + state.has_all({ + "White Trinity", + "Forget-Me-Not"}, player) + )) + add_rule(kh1world.get_location("Halloween Town Bridge Under Bridge"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + )) + add_rule(kh1world.get_location("Halloween Town Boneyard Tombstone Puzzle Chest"), + lambda state: state.has("Forget-Me-Not", player)) + add_rule(kh1world.get_location("Halloween Town Bridge Right of Gate Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and + ( + state.has("Progressive Glide", player) + or options.advanced_logic + ) + )) + add_rule(kh1world.get_location("Halloween Town Cemetery Behind Grave Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Cemetery By Cat Shape Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Cemetery Between Graves Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Lower Iron Cage Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Upper Iron Cage Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Hollow Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Grounds Red Trinity Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not", + "Red Trinity"}, player) + )) + add_rule(kh1world.get_location("Halloween Town Guillotine Square High Tower Chest"), + lambda state: ( + state.has("High Jump", player) + or (options.advanced_logic and can_dumbo_skip(state, player)) + or (options.advanced_logic and state.has("Progressive Glide", player)) + )) + add_rule(kh1world.get_location("Halloween Town Guillotine Square Pumpkin Structure Left Chest"), + lambda state: ( + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and + ( + state.has("Progressive Glide", player) + or (options.advanced_logic and state.has("Combo Master", player)) + or state.has("High Jump", player, 2) + ) + )) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Entrance Steps Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + )) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Inside Entrance Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + )) + add_rule(kh1world.get_location("Halloween Town Bridge Left of Gate Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and + ( + state.has("Progressive Glide", player) + or state.has("High Jump", player) + or options.advanced_logic + ) + )) + add_rule(kh1world.get_location("Halloween Town Cemetery By Striped Grave Chest"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Guillotine Square Pumpkin Structure Right Chest"), + lambda state: ( + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and + ( + state.has("Progressive Glide", player) + or (options.advanced_logic and state.has("Combo Master", player)) + or state.has("High Jump", player, 2) + ) + )) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Right Blue Trinity Chest"), + lambda state: state.has("Blue Trinity", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Left Blue Trinity Chest"), + lambda state: state.has("Blue Trinity", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates White Trinity Chest"), + lambda state: state.has("White Trinity", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Blizzara Chest"), + lambda state: state.has("Progressive Blizzard", player, 2)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Blizzaga Chest"), + lambda state: state.has("Progressive Blizzard", player, 3)) + add_rule(kh1world.get_location("Monstro Mouth High Platform Boat Side Chest"), + lambda state: ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("Monstro Mouth High Platform Across from Boat Chest"), + lambda state: ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("Monstro Mouth Green Trinity Top of Boat Chest"), + lambda state: ( + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + and state.has("Green Trinity", player) + )) + add_rule(kh1world.get_location("Monstro Chamber 5 Platform Chest"), + lambda state: state.has("High Jump", player)) + add_rule(kh1world.get_location("Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest"), + lambda state: ( + state.has("High Jump", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest"), + lambda state: ( + state.has("High Jump", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Monstro Chamber 5 Atop Barrel Chest"), + lambda state: ( + state.has("High Jump", player) + or options.advanced_logic + )) + add_rule(kh1world.get_location("Neverland Pirate Ship Deck White Trinity Chest"), + lambda state: ( + state.has_all({ + "White Trinity", + "Green Trinity"}, player) + )) + add_rule(kh1world.get_location("Neverland Pirate Ship Crows Nest Chest"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Neverland Hold Yellow Trinity Right Blue Chest"), + lambda state: state.has("Yellow Trinity", player)) + add_rule(kh1world.get_location("Neverland Hold Yellow Trinity Left Blue Chest"), + lambda state: state.has("Yellow Trinity", player)) + add_rule(kh1world.get_location("Neverland Cabin Chest"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Neverland Hold Flight 1st Chest"), + lambda state: ( + state.has("Green Trinity", player) + or state.has("Progressive Glide", player) + or state.has("High Jump", player, 3) + )) + add_rule(kh1world.get_location("Neverland Clock Tower Chest"), + lambda state: ( + state.has("Green Trinity", player) + and has_all_magic_lvx(state, player, 2) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"), + lambda state: ( + state.has("Green Trinity", player) + or state.has("Progressive Glide", player) + or state.has("High Jump", player, 3) + )) + add_rule(kh1world.get_location("Neverland Hold Yellow Trinity Green Chest"), + lambda state: state.has("Yellow Trinity", player)) + add_rule(kh1world.get_location("Neverland Captain's Cabin Chest"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Under Water 2nd Chest"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Floating Platform Near Save Chest"), + lambda state: ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + or state.has("Progressive Blizzard", player) + )) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Floating Platform Near Bubble Chest"), + lambda state: ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + or state.has("Progressive Blizzard", player) + )) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls High Platform Chest"), + lambda state: ( + state.has("Progressive Glide", player) + or (state.has("Progressive Blizzard", player) and has_emblems(state, player, options.keyblades_unlock_chests)) + or (options.advanced_logic and state.has("Combo Master", player)) + )) + add_rule(kh1world.get_location("Hollow Bastion Castle Gates Gravity Chest"), + lambda state: ( + state.has("Progressive Gravity", player) + and + ( + has_emblems(state, player, options.keyblades_unlock_chests) + or (options.advanced_logic and state.has("High Jump", player, 2) and state.has("Progressive Glide", player)) + or (options.advanced_logic and can_dumbo_skip(state, player) and state.has("Progressive Glide", player)) + ) + )) + add_rule(kh1world.get_location("Hollow Bastion Castle Gates Freestanding Pillar Chest"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) + or state.has("High Jump", player, 2) + or (options.advanced_logic and can_dumbo_skip(state, player)) + )) + add_rule(kh1world.get_location("Hollow Bastion Castle Gates High Pillar Chest"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) + or state.has("High Jump", player, 2) + or (options.advanced_logic and can_dumbo_skip(state, player)) + )) + add_rule(kh1world.get_location("Hollow Bastion Great Crest Lower Chest"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Great Crest After Battle Platform Chest"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion High Tower 2nd Gravity Chest"), + lambda state: ( + state.has("Progressive Gravity", player) + and has_emblems(state, player, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hollow Bastion High Tower 1st Gravity Chest"), + lambda state: ( + state.has("Progressive Gravity", player) + and has_emblems(state, player, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hollow Bastion High Tower Above Sliding Blocks Chest"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest"), + lambda state: ( + state.has("Progressive Gravity", player) + and has_emblems(state, player, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Library Node Gravity Chest"), + lambda state: state.has("Progressive Gravity", player)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) + and state.has_all({ + "Progressive Glide", + "Progressive Gravity"}, player) + )) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Outside Library Gravity Chest"), + lambda state: state.has("Progressive Gravity", player)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest"), + lambda state: ( + state.has("Progressive Gravity", player) + and has_emblems(state, player, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hollow Bastion Waterway Blizzard on Bubble Chest"), + lambda state: ( + (state.has("Progressive Blizzard", player) and state.has("High Jump", player)) + or state.has("High Jump", player, 3) + )) + add_rule(kh1world.get_location("Hollow Bastion Grand Hall Steps Right Side Chest"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Grand Hall Oblivion Chest"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Grand Hall Left of Gate Chest"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Entrance Hall Left of Emblem Door Chest"), + lambda state: ( + state.has("High Jump", player) + or + ( + options.advanced_logic + and can_dumbo_skip(state, player) + and has_emblems(state, player, options.keyblades_unlock_chests) + ) + )) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls White Trinity Chest"), + lambda state: state.has("White Trinity", player)) + add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"), + lambda state: ( + state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"), + lambda state: ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("End of the World Giant Crevasse 4th Chest"), + lambda state: ( + ( + options.advanced_logic + and state.has("High Jump", player) + and state.has("Combo Master", player) + ) + or state.has("Progressive Glide", player) + )) + add_rule(kh1world.get_location("End of the World World Terminus Agrabah Chest"), + lambda state: ( + state.has("High Jump", player) + or + ( + options.advanced_logic + and can_dumbo_skip(state, player) + and state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("Monstro Chamber 6 White Trinity Chest"), + lambda state: state.has("White Trinity", player)) + add_rule(kh1world.get_location("Traverse Town Kairi Secret Waterway Oathkeeper Event"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) + and state.has("Hollow Bastion", player) + and has_x_worlds(state, player, 5, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Deep Jungle Defeat Sabor White Fang Event"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Defeat Clayton Cure Event"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Seal Keyhole Jungle King Event"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Seal Keyhole Red Trinity Event"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Olympus Coliseum Defeat Cerberus Inferno Band Event"), + lambda state: state.has("Entry Pass", player)) + add_rule(kh1world.get_location("Olympus Coliseum Cloud Sonic Blade Event"), + lambda state: state.has("Entry Pass", player)) + add_rule(kh1world.get_location("Wonderland Defeat Trickmaster Blizzard Event"), + lambda state: state.has("Footprints", player)) + add_rule(kh1world.get_location("Wonderland Defeat Trickmaster Ifrit's Horn Event"), + lambda state: state.has("Footprints", player)) + add_rule(kh1world.get_location("Monstro Defeat Parasite Cage II Stop Event"), + lambda state: ( + state.has("High Jump", player) + or + ( + options.advanced_logic + and state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("Halloween Town Defeat Oogie Boogie Holy Circlet Event"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Defeat Oogie's Manor Gravity Event"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Halloween Town Seal Keyhole Pumpkinhead Event"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not"}, player) + and has_oogie_manor(state, player, options.advanced_logic) + )) + add_rule(kh1world.get_location("Neverland Defeat Anti Sora Raven's Claw Event"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Neverland Encounter Hook Cure Event"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Neverland Seal Keyhole Fairy Harp Event"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Neverland Seal Keyhole Tinker Bell Event"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Neverland Seal Keyhole Glide Event"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Neverland Defeat Captain Hook Ars Arcanum Event"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Hollow Bastion Defeat Maleficent Donald Cheer Event"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Defeat Dragon Maleficent Fireglow Event"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Defeat Riku II Ragnarok Event"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Defeat Behemoth Omega Arts Event"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Speak to Princesses Fire Event"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 01 Event"), + lambda state: state.has("Postcard", player)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 02 Event"), + lambda state: state.has("Postcard", player, 2)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 03 Event"), + lambda state: state.has("Postcard", player, 3)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 04 Event"), + lambda state: state.has("Postcard", player, 4)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 05 Event"), + lambda state: state.has("Postcard", player, 5)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 06 Event"), + lambda state: state.has("Postcard", player, 6)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 07 Event"), + lambda state: state.has("Postcard", player, 7)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 08 Event"), + lambda state: state.has("Postcard", player, 8)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 09 Event"), + lambda state: state.has("Postcard", player, 9)) + add_rule(kh1world.get_location("Traverse Town Mail Postcard 10 Event"), + lambda state: state.has("Postcard", player, 10)) + add_rule(kh1world.get_location("Traverse Town Defeat Opposite Armor Aero Event"), + lambda state: state.has("Red Trinity", player)) + add_rule(kh1world.get_location("Hollow Bastion Speak with Aerith Ansem's Report 2"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Speak with Aerith Ansem's Report 4"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Defeat Maleficent Ansem's Report 5"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Speak with Aerith Ansem's Report 6"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Halloween Town Defeat Oogie Boogie Ansem's Report 7"), + lambda state: ( + state.has_all({ + "Jack-In-The-Box", + "Forget-Me-Not", + "Progressive Fire"}, player) + )) + add_rule(kh1world.get_location("Neverland Defeat Hook Ansem's Report 9"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Hollow Bastion Speak with Aerith Ansem's Report 10"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Geppetto Reward 1"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Geppetto Reward 2"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Geppetto Reward 3"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Geppetto Reward 4"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Geppetto Reward 5"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Geppetto All Summons Reward"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_all_summons(state, player) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Talk to Pinocchio"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Magician's Study Obtained All Arts Items"), + lambda state: ( + has_all_magic_lvx(state, player, 1) + and has_all_arts(state, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Traverse Town Magician's Study Obtained All LV1 Magic"), + lambda state: has_all_magic_lvx(state, player, 1)) + add_rule(kh1world.get_location("Traverse Town Magician's Study Obtained All LV3 Magic"), + lambda state: has_all_magic_lvx(state, player, 3)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 10 Puppies"), + lambda state: has_puppies(state, player, 10)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 20 Puppies"), + lambda state: has_puppies(state, player, 20)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 30 Puppies"), + lambda state: has_puppies(state, player, 30)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 40 Puppies"), + lambda state: has_puppies(state, player, 40)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 50 Puppies Reward 1"), + lambda state: has_puppies(state, player, 50)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 50 Puppies Reward 2"), + lambda state: has_puppies(state, player, 50)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 60 Puppies"), + lambda state: has_puppies(state, player, 60)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 70 Puppies"), + lambda state: has_puppies(state, player, 70)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 80 Puppies"), + lambda state: has_puppies(state, player, 80)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 90 Puppies"), + lambda state: has_puppies(state, player, 90)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 99 Puppies Reward 1"), + lambda state: has_puppies(state, player, 99)) + add_rule(kh1world.get_location("Traverse Town Piano Room Return 99 Puppies Reward 2"), + lambda state: has_puppies(state, player, 99)) + add_rule(kh1world.get_location("Neverland Hold Aero Chest"), + lambda state: state.has("Yellow Trinity", player)) + add_rule(kh1world.get_location("Deep Jungle Camp Hi-Potion Experiment"), + lambda state: state.has("Progressive Fire", player)) + add_rule(kh1world.get_location("Deep Jungle Camp Ether Experiment"), + lambda state: state.has("Progressive Blizzard", player)) + add_rule(kh1world.get_location("Deep Jungle Camp Replication Experiment"), + lambda state: state.has("Progressive Blizzard", player)) + add_rule(kh1world.get_location("Deep Jungle Cliff Save Gorillas"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Tree House Save Gorillas"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Camp Save Gorillas"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Bamboo Thicket Save Gorillas"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Climbing Trees Save Gorillas"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Jungle Slider 10 Fruits"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Jungle Slider 20 Fruits"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Jungle Slider 30 Fruits"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Jungle Slider 40 Fruits"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Deep Jungle Jungle Slider 50 Fruits"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Wonderland Bizarre Room Read Book"), + lambda state: state.has("Footprints", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Green Trinity"), + lambda state: state.has("Green Trinity", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Hero's License Event"), + lambda state: state.has("Entry Pass", player)) + add_rule(kh1world.get_location("Deep Jungle Cavern of Hearts Navi-G Piece Event"), + lambda state: state.has("Slides", player)) + add_rule(kh1world.get_location("Wonderland Bizarre Room Navi-G Piece Event"), + lambda state: state.has("Footprints", player)) + add_rule(kh1world.get_location("Traverse Town Synth Log"), + lambda state: ( + state.has("Empty Bottle", player, 6) + and + ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + ) + )) + add_rule(kh1world.get_location("Traverse Town Synth Cloth"), + lambda state: ( + state.has("Empty Bottle", player, 6) + and + ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + ) + )) + add_rule(kh1world.get_location("Traverse Town Synth Rope"), + lambda state: ( + state.has("Empty Bottle", player, 6) + and + ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + ) + )) + add_rule(kh1world.get_location("Traverse Town Synth Seagull Egg"), + lambda state: ( + state.has("Empty Bottle", player, 6) + and + ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + ) + )) + add_rule(kh1world.get_location("Traverse Town Synth Fish"), + lambda state: ( + state.has("Empty Bottle", player, 6) + and + ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + ) + )) + add_rule(kh1world.get_location("Traverse Town Synth Mushroom"), + lambda state: ( + state.has("Empty Bottle", player, 6) + and + ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + ) + )) + add_rule(kh1world.get_location("Traverse Town Gizmo Shop Postcard 1"), + lambda state: state.has("Progressive Thunder", player)) + add_rule(kh1world.get_location("Traverse Town Gizmo Shop Postcard 2"), + lambda state: state.has("Progressive Thunder", player)) + add_rule(kh1world.get_location("Traverse Town Item Workshop Postcard"), + lambda state: ( + state.has("Green Trinity", player) + or state.has("High Jump", player, 3) + )) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Postcard"), + lambda state: ( + state.has("Monstro", player) + and + ( + state.has("High Jump", player) + or (options.advanced_logic and state.has("Progressive Glide", player)) + ) + and has_x_worlds(state, player, 2, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hollow Bastion Entrance Hall Emblem Piece (Flame)"), + lambda state: ( + ( + state.has("Theon Vol. 6", player) + or state.has("High Jump", player, 3) + or has_emblems(state, player, options.keyblades_unlock_chests) + ) + and state.has("Progressive Fire", player) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + or state.has("Progressive Thunder", player) + or options.advanced_logic + ) + )) + add_rule(kh1world.get_location("Hollow Bastion Entrance Hall Emblem Piece (Chest)"), + lambda state: ( + state.has("Theon Vol. 6", player) + or state.has("High Jump", player, 3) + or has_emblems(state, player, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hollow Bastion Entrance Hall Emblem Piece (Statue)"), + lambda state: ( + ( + state.has("Theon Vol. 6", player) + or state.has("High Jump", player, 3) + or has_emblems(state, player, options.keyblades_unlock_chests) + ) + and state.has("Red Trinity", player) + )) + add_rule(kh1world.get_location("Hollow Bastion Entrance Hall Emblem Piece (Fountain)"), + lambda state: ( + state.has("Theon Vol. 6", player) + or state.has("High Jump", player, 3) + or has_emblems(state, player, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hollow Bastion Library Speak to Belle Divine Rose"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + add_rule(kh1world.get_location("Hollow Bastion Library Speak to Aerith Cure"), + lambda state: has_emblems(state, player, options.keyblades_unlock_chests)) + if options.hundred_acre_wood: + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Left Cliff Chest"), + lambda state: ( + has_torn_pages(state, player, 4) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Right Tree Alcove Chest"), + lambda state: ( + has_torn_pages(state, player, 4) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Under Giant Pot Chest"), + lambda state: has_torn_pages(state, player, 4)) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Turn in Rare Nut 1"), + lambda state: has_torn_pages(state, player, 4)) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Turn in Rare Nut 2"), + lambda state: ( + has_torn_pages(state, player, 4) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Turn in Rare Nut 3"), + lambda state: ( + has_torn_pages(state, player, 4) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Turn in Rare Nut 4"), + lambda state: ( + has_torn_pages(state, player, 4) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Turn in Rare Nut 5"), + lambda state: ( + has_torn_pages(state, player, 4) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + )) + add_rule(kh1world.get_location("100 Acre Wood Pooh's House Owl Cheer"), + lambda state: has_torn_pages(state, player, 5)) + add_rule(kh1world.get_location("100 Acre Wood Convert Torn Page 1"), + lambda state: has_torn_pages(state, player, 1)) + add_rule(kh1world.get_location("100 Acre Wood Convert Torn Page 2"), + lambda state: has_torn_pages(state, player, 2)) + add_rule(kh1world.get_location("100 Acre Wood Convert Torn Page 3"), + lambda state: has_torn_pages(state, player, 3)) + add_rule(kh1world.get_location("100 Acre Wood Convert Torn Page 4"), + lambda state: has_torn_pages(state, player, 4)) + add_rule(kh1world.get_location("100 Acre Wood Convert Torn Page 5"), + lambda state: has_torn_pages(state, player, 5)) + add_rule(kh1world.get_location("100 Acre Wood Pooh's House Start Fire"), + lambda state: has_torn_pages(state, player, 3)) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Break Log"), + lambda state: has_torn_pages(state, player, 4)) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Fall Through Top of Tree Next to Pooh"), + lambda state: ( + has_torn_pages(state, player, 4) + and + ( + state.has("High Jump", player) + or state.has("Progressive Glide", player) + ) + )) + if options.atlantica: + add_rule(kh1world.get_location("Atlantica Ursula's Lair Use Fire on Urchin Chest"), + lambda state: ( + state.has_all({ + "Progressive Fire", + "Crystal Trident"}, player) + )) + add_rule(kh1world.get_location("Atlantica Triton's Palace White Trinity Chest"), + lambda state: state.has("White Trinity", player)) + add_rule(kh1world.get_location("Atlantica Defeat Ursula I Mermaid Kick Event"), + lambda state: ( + has_offensive_magic(state, player) + and state.has("Crystal Trident", player) + )) + add_rule(kh1world.get_location("Atlantica Defeat Ursula II Thunder Event"), + lambda state: ( + state.has("Mermaid Kick", player) + and has_offensive_magic(state, player) + and state.has("Crystal Trident", player) + )) + add_rule(kh1world.get_location("Atlantica Seal Keyhole Crabclaw Event"), + lambda state: ( + state.has("Mermaid Kick", player) + and has_offensive_magic(state, player) + and state.has("Crystal Trident", player) + )) + add_rule(kh1world.get_location("Atlantica Undersea Gorge Blizzard Clam"), + lambda state: state.has("Progressive Blizzard", player)) + add_rule(kh1world.get_location("Atlantica Undersea Valley Fire Clam"), + lambda state: state.has("Progressive Fire", player)) + add_rule(kh1world.get_location("Atlantica Triton's Palace Thunder Clam"), + lambda state: state.has("Progressive Thunder", player)) + add_rule(kh1world.get_location("Atlantica Cavern Nook Clam"), + lambda state: state.has("Crystal Trident", player)) + add_rule(kh1world.get_location("Atlantica Defeat Ursula II Ansem's Report 3"), + lambda state: ( + state.has_all({ + "Mermaid Kick", + "Crystal Trident"}, player) + and has_offensive_magic(state, player) + )) + if options.cups: + add_rule(kh1world.get_location("Olympus Coliseum Defeat Hades Ansem's Report 8"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Complete Phil Cup"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Entry Pass"}, player) + )) + add_rule(kh1world.get_location("Complete Phil Cup Solo"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Entry Pass"}, player) + )) + add_rule(kh1world.get_location("Complete Phil Cup Time Trial"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Entry Pass"}, player) + )) + add_rule(kh1world.get_location("Complete Pegasus Cup"), + lambda state: ( + state.has_all({ + "Pegasus Cup", + "Entry Pass"}, player) + )) + add_rule(kh1world.get_location("Complete Pegasus Cup Solo"), + lambda state: ( + state.has_all({ + "Pegasus Cup", + "Entry Pass"}, player) + )) + add_rule(kh1world.get_location("Complete Pegasus Cup Time Trial"), + lambda state: ( + state.has_all({ + "Pegasus Cup", + "Entry Pass"}, player) + )) + add_rule(kh1world.get_location("Complete Hercules Cup"), + lambda state: ( + state.has_all({ + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 4, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Complete Hercules Cup Solo"), + lambda state: ( + state.has_all({ + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 4, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Complete Hercules Cup Time Trial"), + lambda state: ( + state.has_all({ + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 4, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Complete Hades Cup"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Complete Hades Cup Solo"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Complete Hades Cup Time Trial"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Hades Cup Defeat Cloud and Leon Event"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Hades Cup Defeat Yuffie Event"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Hades Cup Defeat Cerberus Event"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Hades Cup Defeat Behemoth Event"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Hades Cup Defeat Hades Event"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Hercules Cup Defeat Cloud Event"), + lambda state: ( + state.has_all({ + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 4, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Hercules Cup Yellow Trinity Event"), + lambda state: ( + state.has_all({ + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 4, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Olympus Coliseum Defeat Ice Titan Diamond Dust Event"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass", + "Guard"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Olympus Coliseum Gates Purple Jar After Defeating Hades"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Olympus Coliseum Olympia Chest"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 4, options.keyblades_unlock_chests) + )) + if options.super_bosses: + add_rule(kh1world.get_location("Neverland Defeat Phantom Stop Event"), + lambda state: ( + state.has("Green Trinity", player) + and has_all_magic_lvx(state, player, 2) + and has_defensive_tools(state, player) + and has_emblems(state, player, options.keyblades_unlock_chests) + )) + add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Ansem's Report 11"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + and state.has("Progressive Blizzard", player, 3) + )) + add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) and state.has("Progressive Blizzard", player, 3) + )) + if options.super_bosses or options.goal.current_key == "sephiroth": + add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth One-Winged Angel Event"), + lambda state: ( + state.has_all({ + "Phil Cup", + "Pegasus Cup", + "Hercules Cup", + "Entry Pass"}, player) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + if options.super_bosses or options.goal.current_key == "unknown": + add_rule(kh1world.get_location("Hollow Bastion Defeat Unknown Ansem's Report 13"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) + and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + add_rule(kh1world.get_location("Hollow Bastion Defeat Unknown EXP Necklace Event"), + lambda state: ( + has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) + and has_defensive_tools(state, player) + )) + for i in range(options.level_checks): + add_rule(kh1world.get_location("Level " + str(i+1).rjust(3,'0')), + lambda state, level_num=i: ( + has_x_worlds(state, player, min(((level_num//10)*2), 8), options.keyblades_unlock_chests) + )) + if options.goal.current_key == "final_ansem": + add_rule(kh1world.get_location("Final Ansem"), + lambda state: ( + has_final_rest_door(state, player, final_rest_door_requirement, final_rest_door_required_reports, options.keyblades_unlock_chests, options.puppies) + )) + if options.keyblades_unlock_chests: + add_rule(kh1world.get_location("Traverse Town 1st District Candle Puzzle Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town 1st District Accessory Shop Roof Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town 2nd District Boots and Shoes Awning Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town 2nd District Rooftop Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town 2nd District Gizmo Shop Facade Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Alleyway Balcony Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Alleyway Blue Room Awning Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Alleyway Corner Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Green Room Clock Puzzle Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Green Room Table Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Red Room Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Mystical House Yellow Trinity Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Accessory Shop Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Secret Waterway White Trinity Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Geppetto's House Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Item Workshop Right Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town 1st District Blue Trinity Balcony Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Mystical House Glide Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Alleyway Behind Crates Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Item Workshop Left Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Traverse Town Secret Waterway Near Stairs Chest"), + lambda state: state.has("Lionheart", player)) + add_rule(kh1world.get_location("Wonderland Rabbit Hole Green Trinity Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Rabbit Hole Defeat Heartless 1 Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Rabbit Hole Defeat Heartless 2 Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Rabbit Hole Defeat Heartless 3 Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Bizarre Room Green Trinity Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Queen's Castle Hedge Left Red Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Queen's Castle Hedge Right Blue Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Queen's Castle Hedge Right Red Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Lotus Forest Thunder Plant Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Lotus Forest Through the Painting Thunder Plant Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Lotus Forest Glide Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Lotus Forest Nut Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Lotus Forest Corner Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Bizarre Room Lamp Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Bear and Clock Puzzle Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Wonderland Lotus Forest Through the Painting White Trinity Chest"), + lambda state: state.has("Lady Luck", player)) + add_rule(kh1world.get_location("Deep Jungle Tree House Beneath Tree House Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Tree House Rooftop Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Hippo's Lagoon Center Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Hippo's Lagoon Left Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Hippo's Lagoon Right Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Vines Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Vines 2 Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Climbing Trees Blue Trinity Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Tunnel Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Cavern of Hearts White Trinity Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Camp Blue Trinity Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Tent Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern Low Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern Middle Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern High Wall Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Waterfall Cavern High Middle Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Cliff Right Cliff Left Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Cliff Right Cliff Right Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Deep Jungle Tree House Suspended Boat Chest"), + lambda state: state.has("Jungle King", player)) + add_rule(kh1world.get_location("Agrabah Plaza By Storage Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Plaza Raised Terrace Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Plaza Top Corner Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Alley Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Bazaar Across Windows Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Bazaar High Corner Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Main Street Right Palace Entrance Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Main Street High Above Alley Entrance Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Main Street High Above Palace Gates Entrance Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Palace Gates Low Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Palace Gates High Opposite Palace Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Palace Gates High Close to Palace Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Storage Green Trinity Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Storage Behind Barrel Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Entrance Left Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Entrance Tall Tower Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Hall High Left Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Hall Near Bottomless Hall Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Bottomless Hall Raised Platform Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Bottomless Hall Pillar Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Treasure Room Across Platforms Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Treasure Room Above Fire Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Relic Chamber Jump from Stairs Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Relic Chamber Stairs Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Dark Chamber Abu Gem Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Dark Chamber Across from Relic Chamber Entrance Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Dark Chamber Bridge Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Dark Chamber Near Save Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Hidden Room Right Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Hidden Room Left Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Aladdin's House Main Street Entrance Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Aladdin's House Plaza Entrance Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Agrabah Cave of Wonders Entrance White Trinity Chest"), + lambda state: state.has("Three Wishes", player)) + add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 6 Low Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Bridge Under Bridge"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Boneyard Tombstone Puzzle Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Bridge Right of Gate Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Cemetery Behind Grave Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Cemetery By Cat Shape Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Cemetery Between Graves Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Lower Iron Cage Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Upper Iron Cage Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Hollow Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Grounds Red Trinity Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Guillotine Square High Tower Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Guillotine Square Pumpkin Structure Left Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Entrance Steps Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Oogie's Manor Inside Entrance Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Bridge Left of Gate Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Cemetery By Striped Grave Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Guillotine Square Under Jack's House Stairs Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Halloween Town Guillotine Square Pumpkin Structure Right Chest"), + lambda state: state.has("Pumpkinhead", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Left Behind Columns Chest"), + lambda state: state.has("Olympia", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Right Blue Trinity Chest"), + lambda state: state.has("Olympia", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Left Blue Trinity Chest"), + lambda state: state.has("Olympia", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates White Trinity Chest"), + lambda state: state.has("Olympia", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Blizzara Chest"), + lambda state: state.has("Olympia", player)) + add_rule(kh1world.get_location("Olympus Coliseum Coliseum Gates Blizzaga Chest"), + lambda state: state.has("Olympia", player)) + add_rule(kh1world.get_location("Monstro Mouth Boat Deck Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Mouth High Platform Boat Side Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Mouth High Platform Across from Boat Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Mouth Near Ship Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Mouth Green Trinity Top of Boat Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 2 Ground Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 2 Platform Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 5 Platform Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 3 Ground Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 3 Near Chamber 6 Entrance Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Mouth High Platform Near Teeth Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 5 Atop Barrel Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 5 Low 2nd Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Monstro Chamber 5 Low 1st Chest"), + lambda state: state.has("Wishing Star", player)) + add_rule(kh1world.get_location("Neverland Pirate Ship Deck White Trinity Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Pirate Ship Crows Nest Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Hold Yellow Trinity Right Blue Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Hold Yellow Trinity Left Blue Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Galley Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Cabin Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Hold Flight 1st Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Clock Tower Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Hold Yellow Trinity Green Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Neverland Captain's Cabin Chest"), + lambda state: state.has("Fairy Harp", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Water's Surface Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Under Water 1st Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Under Water 2nd Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Floating Platform Near Save Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls Floating Platform Near Bubble Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls High Platform Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Castle Gates Gravity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Castle Gates Freestanding Pillar Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Castle Gates High Pillar Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Great Crest Lower Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Great Crest After Battle Platform Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion High Tower 2nd Gravity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion High Tower 1st Gravity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion High Tower Above Sliding Blocks Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Library Top of Bookshelf Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Library Node Gravity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Outside Library Gravity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Base Level Bubble Under the Wall Platform Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Base Level Platform Near Entrance Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Base Level Near Crystal Switch Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Waterway Near Save Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Waterway Blizzard on Bubble Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Waterway Unlock Passage from Base Level Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Dungeon By Candles Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Dungeon Corner Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Grand Hall Steps Right Side Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Grand Hall Oblivion Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Grand Hall Left of Gate Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Entrance Hall Left of Emblem Door Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("Hollow Bastion Rising Falls White Trinity Chest"), + lambda state: state.has("Divine Rose", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 1st Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 2nd Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 3rd Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 4th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 5th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 6th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 10th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 9th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 8th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Dimension 7th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Giant Crevasse 3rd Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Giant Crevasse 4th Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Giant Crevasse 2nd Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus Traverse Town Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus Wonderland Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus Olympus Coliseum Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus Deep Jungle Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus Agrabah Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus Halloween Town Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus Neverland Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World World Terminus 100 Acre Wood Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("End of the World Final Rest Chest"), + lambda state: state.has("Oblivion", player)) + add_rule(kh1world.get_location("Monstro Chamber 6 White Trinity Chest"), + lambda state: state.has("Oblivion", player)) + if options.hundred_acre_wood: + add_rule(kh1world.get_location("100 Acre Wood Meadow Inside Log Chest"), + lambda state: state.has("Oathkeeper", player)) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Left Cliff Chest"), + lambda state: state.has("Oathkeeper", player)) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Right Tree Alcove Chest"), + lambda state: state.has("Oathkeeper", player)) + add_rule(kh1world.get_location("100 Acre Wood Bouncing Spot Under Giant Pot Chest"), + lambda state: state.has("Oathkeeper", player)) + + + add_rule(kh1world.get_entrance("Wonderland"), + lambda state: state.has("Wonderland", player) and has_x_worlds(state, player, 2, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("Olympus Coliseum"), + lambda state: state.has("Olympus Coliseum", player) and has_x_worlds(state, player, 2, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("Deep Jungle"), + lambda state: state.has("Deep Jungle", player) and has_x_worlds(state, player, 2, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("Agrabah"), + lambda state: state.has("Agrabah", player) and has_x_worlds(state, player, 2, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("Monstro"), + lambda state: state.has("Monstro", player) and has_x_worlds(state, player, 2, options.keyblades_unlock_chests)) + if options.atlantica: + add_rule(kh1world.get_entrance("Atlantica"), + lambda state: state.has("Atlantica", player) and has_x_worlds(state, player, 2, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("Halloween Town"), + lambda state: state.has("Halloween Town", player) and has_x_worlds(state, player, 2, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("Neverland"), + lambda state: state.has("Neverland", player) and has_x_worlds(state, player, 3, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("Hollow Bastion"), + lambda state: state.has("Hollow Bastion", player) and has_x_worlds(state, player, 5, options.keyblades_unlock_chests)) + add_rule(kh1world.get_entrance("End of the World"), + lambda state: has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and (has_reports(state, player, eotw_required_reports) or state.has("End of the World", player))) + add_rule(kh1world.get_entrance("100 Acre Wood"), + lambda state: state.has("Progressive Fire", player)) + + multiworld.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/kh1/__init__.py b/worlds/kh1/__init__.py new file mode 100644 index 000000000000..63b457556894 --- /dev/null +++ b/worlds/kh1/__init__.py @@ -0,0 +1,282 @@ +import logging +from typing import List + +from BaseClasses import Tutorial +from worlds.AutoWorld import WebWorld, World +from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups +from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups +from .Options import KH1Options, kh1_option_groups +from .Regions import create_regions +from .Rules import set_rules +from .Presets import kh1_option_presets +from worlds.LauncherComponents import Component, components, Type, launch_subprocess + + +def launch_client(): + from .Client import launch + launch_subprocess(launch, name="KH1 Client") + + +components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT)) + + +class KH1Web(WebWorld): + theme = "ocean" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Kingdom Hearts Randomizer software on your computer." + "This guide covers single-player, multiworld, and related software.", + "English", + "kh1_en.md", + "kh1/en", + ["Gicu"] + )] + option_groups = kh1_option_groups + options_presets = kh1_option_presets + + +class KH1World(World): + """ + Kingdom Hearts is an action RPG following Sora on his journey + through many worlds to find Riku and Kairi. + """ + game = "Kingdom Hearts" + options_dataclass = KH1Options + options: KH1Options + topology_present = True + web = KH1Web() + + 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()} + item_name_groups = item_name_groups + location_name_groups = location_name_groups + fillers = {} + fillers.update(get_items_by_category("Item")) + fillers.update(get_items_by_category("Camping")) + fillers.update(get_items_by_category("Stat Ups")) + + def create_items(self): + self.place_predetermined_items() + # Handle starting worlds + starting_worlds = [] + if self.options.starting_worlds > 0: + possible_starting_worlds = ["Wonderland", "Olympus Coliseum", "Deep Jungle", "Agrabah", "Monstro", "Halloween Town", "Neverland", "Hollow Bastion"] + if self.options.atlantica: + possible_starting_worlds.append("Atlantica") + if self.options.end_of_the_world_unlock == "item": + possible_starting_worlds.append("End of the World") + starting_worlds = self.random.sample(possible_starting_worlds, min(self.options.starting_worlds.value, len(possible_starting_worlds))) + for starting_world in starting_worlds: + self.multiworld.push_precollected(self.create_item(starting_world)) + + item_pool: List[KH1Item] = [] + possible_level_up_item_pool = [] + level_up_item_pool = [] + + # Calculate Level Up Items + # Fill pool with mandatory items + for _ in range(self.options.item_slot_increase): + level_up_item_pool.append("Item Slot Increase") + for _ in range(self.options.accessory_slot_increase): + level_up_item_pool.append("Accessory Slot Increase") + + # Create other pool + for _ in range(self.options.strength_increase): + possible_level_up_item_pool.append("Strength Increase") + for _ in range(self.options.defense_increase): + possible_level_up_item_pool.append("Defense Increase") + for _ in range(self.options.hp_increase): + possible_level_up_item_pool.append("Max HP Increase") + for _ in range(self.options.mp_increase): + possible_level_up_item_pool.append("Max MP Increase") + for _ in range(self.options.ap_increase): + possible_level_up_item_pool.append("Max AP Increase") + + # Fill remaining pool with items from other pool + self.random.shuffle(possible_level_up_item_pool) + level_up_item_pool = level_up_item_pool + possible_level_up_item_pool[:(100 - len(level_up_item_pool))] + + level_up_locations = list(get_locations_by_category("Levels").keys()) + self.random.shuffle(level_up_item_pool) + current_level_for_placing_stats = self.options.force_stats_on_levels.value + while len(level_up_item_pool) > 0 and current_level_for_placing_stats <= self.options.level_checks: + self.get_location(level_up_locations[current_level_for_placing_stats - 1]).place_locked_item(self.create_item(level_up_item_pool.pop())) + current_level_for_placing_stats += 1 + + # Calculate prefilled locations and items + prefilled_items = [] + if self.options.vanilla_emblem_pieces: + prefilled_items = prefilled_items + ["Emblem Piece (Flame)", "Emblem Piece (Chest)", "Emblem Piece (Fountain)", "Emblem Piece (Statue)"] + + total_locations = len(self.multiworld.get_unfilled_locations(self.player)) + + non_filler_item_categories = ["Key", "Magic", "Worlds", "Trinities", "Cups", "Summons", "Abilities", "Shared Abilities", "Keyblades", "Accessory", "Weapons", "Puppies"] + if self.options.hundred_acre_wood: + non_filler_item_categories.append("Torn Pages") + for name, data in item_table.items(): + quantity = data.max_quantity + if data.category not in non_filler_item_categories: + continue + if name in starting_worlds: + continue + if data.category == "Puppies": + if self.options.puppies == "triplets" and "-" in name: + item_pool += [self.create_item(name) for _ in range(quantity)] + if self.options.puppies == "individual" and "Puppy" in name: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + if self.options.puppies == "full" and name == "All Puppies": + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name == "Atlantica": + if self.options.atlantica: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name == "Mermaid Kick": + if self.options.atlantica: + if self.options.extra_shared_abilities: + item_pool += [self.create_item(name) for _ in range(0, 2)] + else: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name == "Crystal Trident": + if self.options.atlantica: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name == "High Jump": + if self.options.extra_shared_abilities: + item_pool += [self.create_item(name) for _ in range(0, 3)] + else: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name == "Progressive Glide": + if self.options.extra_shared_abilities: + item_pool += [self.create_item(name) for _ in range(0, 4)] + else: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name == "End of the World": + if self.options.end_of_the_world_unlock.current_key == "item": + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name == "EXP Zero": + if self.options.exp_zero_in_pool: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + elif name not in prefilled_items: + item_pool += [self.create_item(name) for _ in range(0, quantity)] + + for i in range(self.determine_reports_in_pool()): + item_pool += [self.create_item("Ansem's Report " + str(i+1))] + + while len(item_pool) < total_locations and len(level_up_item_pool) > 0: + item_pool += [self.create_item(level_up_item_pool.pop())] + + # Fill any empty locations with filler items. + while len(item_pool) < total_locations: + item_pool.append(self.create_item(self.get_filler_item_name())) + + self.multiworld.itempool += item_pool + + def place_predetermined_items(self) -> None: + goal_dict = { + "sephiroth": "Olympus Coliseum Defeat Sephiroth Ansem's Report 12", + "unknown": "Hollow Bastion Defeat Unknown Ansem's Report 13", + "postcards": "Traverse Town Mail Postcard 10 Event", + "final_ansem": "Final Ansem", + "puppies": "Traverse Town Piano Room Return 99 Puppies Reward 2", + "final_rest": "End of the World Final Rest Chest" + } + self.get_location(goal_dict[self.options.goal.current_key]).place_locked_item(self.create_item("Victory")) + if self.options.vanilla_emblem_pieces: + self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Flame)").place_locked_item(self.create_item("Emblem Piece (Flame)")) + self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Statue)").place_locked_item(self.create_item("Emblem Piece (Statue)")) + self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Fountain)").place_locked_item(self.create_item("Emblem Piece (Fountain)")) + self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Chest)").place_locked_item(self.create_item("Emblem Piece (Chest)")) + + def get_filler_item_name(self) -> str: + weights = [data.weight for data in self.fillers.values()] + return self.random.choices([filler for filler in self.fillers.keys()], weights)[0] + + def fill_slot_data(self) -> dict: + slot_data = {"xpmult": int(self.options.exp_multiplier)/16, + "required_reports_eotw": self.determine_reports_required_to_open_end_of_the_world(), + "required_reports_door": self.determine_reports_required_to_open_final_rest_door(), + "door": self.options.final_rest_door.current_key, + "seed": self.multiworld.seed_name, + "advanced_logic": bool(self.options.advanced_logic), + "hundred_acre_wood": bool(self.options.hundred_acre_wood), + "atlantica": bool(self.options.atlantica), + "goal": str(self.options.goal.current_key)} + if self.options.randomize_keyblade_stats: + min_str_bonus = min(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value) + max_str_bonus = max(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value) + self.options.keyblade_min_str.value = min_str_bonus + self.options.keyblade_max_str.value = max_str_bonus + min_mp_bonus = min(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value) + max_mp_bonus = max(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value) + self.options.keyblade_min_mp.value = min_mp_bonus + self.options.keyblade_max_mp.value = max_mp_bonus + slot_data["keyblade_stats"] = "" + for i in range(22): + if i < 4 and self.options.bad_starting_weapons: + slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + "1,0," + else: + str_bonus = int(self.random.randint(min_str_bonus, max_str_bonus)) + mp_bonus = int(self.random.randint(min_mp_bonus, max_mp_bonus)) + slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + str(str_bonus) + "," + str(mp_bonus) + "," + slot_data["keyblade_stats"] = slot_data["keyblade_stats"][:-1] + if self.options.donald_death_link: + slot_data["donalddl"] = "" + if self.options.goofy_death_link: + slot_data["goofydl"] = "" + if self.options.keyblades_unlock_chests: + slot_data["chestslocked"] = "" + else: + slot_data["chestsunlocked"] = "" + if self.options.interact_in_battle: + slot_data["interactinbattle"] = "" + return slot_data + + def create_item(self, name: str) -> KH1Item: + data = item_table[name] + return KH1Item(name, data.classification, data.code, self.player) + + def create_event(self, name: str) -> KH1Item: + data = event_item_table[name] + return KH1Item(name, data.classification, data.code, self.player) + + def set_rules(self): + set_rules(self) + + def create_regions(self): + create_regions(self.multiworld, self.player, self.options) + + def generate_early(self): + value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"] + initial_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value] + self.change_numbers_of_reports_to_consider() + new_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value] + for i in range(3): + if initial_report_settings[i] != new_report_settings[i]: + logging.info(f"{self.player_name}'s value {initial_report_settings[i]} for \"{value_names[i]}\" was invalid\n" + f"Setting \"{value_names[i]}\" value to {new_report_settings[i]}") + + def change_numbers_of_reports_to_consider(self) -> None: + if self.options.end_of_the_world_unlock == "reports" and self.options.final_rest_door == "reports": + self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted( + [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]) + + elif self.options.end_of_the_world_unlock == "reports": + self.options.required_reports_eotw.value, self.options.reports_in_pool.value = sorted( + [self.options.required_reports_eotw.value, self.options.reports_in_pool.value]) + + elif self.options.final_rest_door == "reports": + self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted( + [self.options.required_reports_door.value, self.options.reports_in_pool.value]) + + def determine_reports_in_pool(self) -> int: + if self.options.end_of_the_world_unlock == "reports" or self.options.final_rest_door == "reports": + return self.options.reports_in_pool.value + return 0 + + def determine_reports_required_to_open_end_of_the_world(self) -> int: + if self.options.end_of_the_world_unlock == "reports": + return self.options.required_reports_eotw.value + return 14 + + def determine_reports_required_to_open_final_rest_door(self) -> int: + if self.options.final_rest_door == "reports": + return self.options.required_reports_door.value + return 14 diff --git a/worlds/kh1/docs/en_Kingdom Hearts.md b/worlds/kh1/docs/en_Kingdom Hearts.md new file mode 100644 index 000000000000..5167505efbbd --- /dev/null +++ b/worlds/kh1/docs/en_Kingdom Hearts.md @@ -0,0 +1,88 @@ +# Kingdom Hearts (PC) + +## Where is the options page? + +The [player options page for this game](../player-options) contains most of the options you need to +configure and export a config file. + +## What does randomization do to this game? + +The Kingdom Hearts AP Randomizer randomizes most rewards in the game and adds several items which are used to unlock worlds, Olympus Coliseum cups, and world progression. + +Worlds can only be accessed by finding the corresponding item. For example, you need to find the `Monstro` item to enter Monstro. + +The default goal is to enter End of the World and defeat Final Ansem. + +## What items and locations get shuffled? + +### Items + +Any weapon, accessory, spell, trinity, summon, world, key item, stat up, consumable, or ability can be found in any location. + +### Locations + +Locations the player can find items include chests, event rewards, Atlantica clams, level up rewards, 101 Dalmatian rewards, and postcard rewards. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit +certain items to your own world. +## When the player receives an item, what happens? + +When the player receives an item, your client will display a message displaying the item you have obtained. You will also see a notification in the "LEVEL UP" box. + +## What do I do if I encounter a bug with the game? + +Please reach out to Gicu#7034 on Discord. + +## How do I progress in a certain world? + +### The evidence boxes aren't spawning in Wonderland. + +Find `Footprints` in the multiworld. + +### I can't enter any cups in Olympus Coliseum. + +Firstly, find `Entry Pass` in the multiworld. Additionally, `Phil Cup`, `Pegasus Cup`, and `Hercules Cup` are all multiworld items. Finding all 3 grant you access to the Hades Cup and the Platinum Match. Clearing all cups lets you challenge Ice Titan. + +### The slides aren't spawning in Deep Jungle. + +Find `Slides` in the multiworld. + +### I can't progress in Atlantica. +Find `Crystal Trident` in the multiworld. + +### I can't progress in Halloween Town. + +Find `Forget-Me-Not` and `Jack-in-the-Box` in the multiworld. + +### The Hollow Bastion Library is missing a book. + +Find `Theon Vol. 6` in the multiworld. + +## How do I enter the End of the World? + +You can enter End of the World by obtaining a number of Ansem's Reports or by finding `End of the World` in the multiworld, depending on your options. + +## Credits +This is a collaborative effort from several individuals in the Kingdom Hearts community, but most of all, denhonator. + +Denho's original KH rando laid the foundation for the work here and makes everything here possible, so thank you Denho for such a blast of a randomizer. + +Other credits include: + +Sonicshadowsilver2 for their work finding many memory addresses, working to identify and resolve bugs, and converting the code base to the latest EGS update. + +Shananas and the rest of the OpenKH team for providing such an amazing tool for us to utilize on this project. + +TopazTK for their work on the `Show Prompt` method and Krujo for their implementation of the method in AP. + +JaredWeakStrike for helping clean up my mess of code. + +KSX for their `Interact in Battle` code. + +RavSpect for their title screen image edit. + +SunCatMC for their work on ChecksFinder, which I used as a basis for game-to-client communication. + +ThePhar for their work on Rogue Legacy AP, which I used as a basis for the apworld creation. diff --git a/worlds/kh1/docs/kh1_en.md b/worlds/kh1/docs/kh1_en.md new file mode 100644 index 000000000000..522da20b0dc9 --- /dev/null +++ b/worlds/kh1/docs/kh1_en.md @@ -0,0 +1,54 @@ +# Kingdom Hearts Randomizer Setup Guide + +## Setting up the required mods + +BEFORE MODDING, PLEASE INSTALL AND RUN KH1 AT LEAST ONCE. + +1. Install OpenKH and the LUA Backend + + Download the [latest release of OpenKH](https://github.com/OpenKH/OpenKh/releases/tag/latest) + + Extract the files to a directory of your choosing. + + Open `OpenKh.Tools.ModsManager.exe` and run first time set up + + When prompted for game edition, choose `PC Release`, select which platform you're using (EGS or Steam), navigate to your `Kingdom Hearts I.5 + II.5` installation folder in the path box and click `Next` + + When prompted, install Panacea, then click `Next` + + When prompted, check KH1 plus any other AP game you play and click `Install and configure LUA backend`, then click `Next` + + Extracting game data for KH1 is unnecessary, but you may want to extract data for KH2 if you plan on playing KH2 AP + + Click `Finish` + +2. Open `OpenKh.Tools.ModsManager.exe` + +3. Click the drop-down menu at the top-right and choose `Kingdom Hearts 1` + +4. Click `Mods>Install a New Mod` + +5. In `Add a new mod from GitHub` paste `gaithern/KH-1FM-AP-LUA` + +6. Click `Install` + +7. Navigate to Mod Loader and click `Build and Run` + + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +you can customize your settings by visiting the [Kingdom Hearts Options Page](/games/Kingdom%20Hearts/player-options). + +## Connect to the MultiWorld + +For first-time players, it is recommended to open your KH1 Client first before opening the game. + +On the title screen, open your KH1 Client and connect to your multiworld. diff --git a/worlds/kh1/test/__init__.py b/worlds/kh1/test/__init__.py new file mode 100644 index 000000000000..960b527f8b8a --- /dev/null +++ b/worlds/kh1/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class KH1TestBase(WorldTestBase): + game = "Kingdom Hearts" diff --git a/worlds/kh1/test/test_goal.py b/worlds/kh1/test/test_goal.py new file mode 100644 index 000000000000..6b501404feee --- /dev/null +++ b/worlds/kh1/test/test_goal.py @@ -0,0 +1,33 @@ +from . import KH1TestBase + +class TestDefault(KH1TestBase): + options = {} + +class TestSephiroth(KH1TestBase): + options = { + "Goal": 0, + } + +class TestUnknown(KH1TestBase): + options = { + "Goal": 1, + } + +class TestPostcards(KH1TestBase): + options = { + "Goal": 2, + } + +class TestFinalAnsem(KH1TestBase): + options = { + "Goal": 3, + } + +class TestPuppies(KH1TestBase): + options = { + "Goal": 4, + } +class TestFinalRest(KH1TestBase): + options = { + "Goal": 5, + } diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 513d85257b97..e2d2338b7651 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -116,12 +116,19 @@ def __init__(self, server_address, password): # self.inBattle = 0x2A0EAC4 + 0x40 # self.onDeath = 0xAB9078 # PC Address anchors - self.Now = 0x0714DB8 - self.Save = 0x09A70B0 + # self.Now = 0x0714DB8 old address + # epic addresses + self.Now = 0x0716DF8 + self.Save = 0x09A92F0 + self.Journal = 0x743260 + self.Shop = 0x743350 + self.Slot1 = 0x2A22FD8 # self.Sys3 = 0x2A59DF0 # self.Bt10 = 0x2A74880 # self.BtlEnd = 0x2A0D3E0 - self.Slot1 = 0x2A20C98 + # self.Slot1 = 0x2A20C98 old address + + self.kh2_game_version = None # can be egs or steam self.chest_set = set(exclusion_table["Chests"]) self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) @@ -228,6 +235,9 @@ def kh2_read_int(self, address): def kh2_write_int(self, address, value): self.kh2.write_int(self.kh2.base_address + address, value) + def kh2_read_string(self, address, length): + return self.kh2.read_string(self.kh2.base_address + address, length) + def on_package(self, cmd: str, args: dict): if cmd in {"RoomInfo"}: self.kh2seedname = args['seed_name'] @@ -367,10 +377,26 @@ def on_package(self, cmd: str, args: dict): for weapon_location in all_weapon_slot: all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location]) self.all_weapon_location_id = set(all_weapon_location_id) + try: self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - logger.info("You are now auto-tracking") - self.kh2connected = True + if self.kh2_game_version is None: + if self.kh2_read_string(0x09A9830, 4) == "KH2J": + self.kh2_game_version = "STEAM" + self.Now = 0x0717008 + self.Save = 0x09A9830 + self.Slot1 = 0x2A23518 + self.Journal = 0x7434E0 + self.Shop = 0x7435D0 + + elif self.kh2_read_string(0x09A92F0, 4) == "KH2J": + self.kh2_game_version = "EGS" + else: + self.kh2_game_version = None + logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") + if self.kh2_game_version is not None: + logger.info(f"You are now auto-tracking. {self.kh2_game_version}") + self.kh2connected = True except Exception as e: if self.kh2connected: @@ -589,8 +615,8 @@ async def IsInShop(self, sellable): # if journal=-1 and shop = 5 then in shop # if journal !=-1 and shop = 10 then journal - journal = self.kh2_read_short(0x741230) - shop = self.kh2_read_short(0x741320) + journal = self.kh2_read_short(self.Journal) + shop = self.kh2_read_short(self.Shop) if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): # print("your in the shop") sellable_dict = {} @@ -599,8 +625,8 @@ async def IsInShop(self, sellable): amount = self.kh2_read_byte(self.Save + itemdata.memaddr) sellable_dict[itemName] = amount while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): - journal = self.kh2_read_short(0x741230) - shop = self.kh2_read_short(0x741320) + journal = self.kh2_read_short(self.Journal) + shop = self.kh2_read_short(self.Shop) await asyncio.sleep(0.5) for item, amount in sellable_dict.items(): itemdata = self.item_name_to_data[item] @@ -750,7 +776,7 @@ async def verifyItems(self): item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(0x741320) in {10, 8}: + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}: self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: @@ -802,7 +828,7 @@ async def verifyItems(self): self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1) elif self.base_item_slots + amount_of_items < 8: self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items) - + # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: @@ -905,8 +931,23 @@ async def kh2_watcher(ctx: KH2Context): await asyncio.sleep(15) ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") if ctx.kh2 is not None: - logger.info("You are now auto-tracking") - ctx.kh2connected = True + if ctx.kh2_game_version is None: + if ctx.kh2_read_string(0x09A9830, 4) == "KH2J": + ctx.kh2_game_version = "STEAM" + ctx.Now = 0x0717008 + ctx.Save = 0x09A9830 + ctx.Slot1 = 0x2A23518 + ctx.Journal = 0x7434E0 + ctx.Shop = 0x7435D0 + + elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J": + ctx.kh2_game_version = "EGS" + else: + ctx.kh2_game_version = None + logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") + if ctx.kh2_game_version is not None: + logger.info(f"You are now auto-tracking {ctx.kh2_game_version}") + ctx.kh2connected = True except Exception as e: if ctx.kh2connected: ctx.kh2connected = False diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 4370ad36b540..0f26b56d0e54 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -355,6 +355,16 @@ def __init__(self, world: KH2World) -> None: RegionName.Master: lambda state: self.multi_form_region_access(), RegionName.Final: lambda state: self.final_form_region_access(state) } + # Accessing Final requires being able to reach one of the locations in final_leveling_access, but reaching a + # location requires being able to reach the region the location is in, so an indirect condition is required. + # The access rules of each of the locations in final_leveling_access do not check for being able to reach other + # locations or other regions, so it is only the parent region of each location that needs to be added as an + # indirect condition. + self.form_region_indirect_condition_regions = { + RegionName.Final: { + self.world.get_location(location).parent_region for location in final_leveling_access + } + } def final_form_region_access(self, state: CollectionState) -> bool: """ @@ -388,12 +398,15 @@ def set_kh2_form_rules(self): for region_name in drive_form_list: if region_name == RegionName.Summon and not self.world.options.SummonLevelLocationToggle: continue + indirect_condition_regions = self.form_region_indirect_condition_regions.get(region_name, ()) # could get the location of each of these, but I feel like that would be less optimal region = self.multiworld.get_region(region_name, self.player) # if region_name in form_region_rules if region_name != RegionName.Summon: for entrance in region.entrances: entrance.access_rule = self.form_region_rules[region_name] + for indirect_condition_region in indirect_condition_regions: + self.multiworld.register_indirect_condition(indirect_condition_region, entrance) for loc in region.locations: loc.access_rule = self.form_rules[loc.name] diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index faf0bed88567..2809460aed6a 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -101,7 +101,18 @@ def fill_slot_data(self) -> dict: if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1: self.goofy_ability_dict[ability] -= 1 - slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired") + slot_data = self.options.as_dict( + "Goal", + "FinalXemnas", + "LuckyEmblemsRequired", + "BountyRequired", + "FightLogic", + "FinalFormLogic", + "AutoFormLogic", + "LevelDepth", + "DonaldGoofyStatsanity", + "CorSkipToggle" + ) slot_data.update({ "hitlist": [], # remove this after next update "PoptrackerVersionCheck": 4.3, diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index c6fdb020b8a4..9fe9b23a1350 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -1,22 +1,25 @@ # Kingdom Hearts 2 Archipelago Setup Guide +

Quick Links

- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en) - [Player Options Page](../../../../games/Kingdom%20Hearts%202/player-options)

Required Software:

- `Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) -- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
- 1. `3.2.0 OpenKH Mod Manager with Panacea`
- 2. `Lua Backend from the OpenKH Mod Manager` - 3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
+`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/) + +- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) + 1. `Version 3.3.0 or greater OpenKH Mod Manager with Panacea` + 2. `Lua Backend from the OpenKH Mod Manager` + 3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager` - Needed for Archipelago - 1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
- 2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
- 3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
- 4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
+ 1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases) + 2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager` + 3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager` + 4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager` 5. `AP Randomizer Seed` +

Required: Archipelago Companion Mod

Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`
@@ -24,6 +27,7 @@ Have this mod second-highest priority below the .zip seed.
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.

Required: Auto Save Mod

+ Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save

Installing A Seed

@@ -32,34 +36,40 @@ When you generate a game you will see a download link for a KH2 .zip seed on the Make sure the seed is on the top of the list (Highest Priority)
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms. +

Optional Software:

+ +- [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/), for use with +[PopTracker](https://github.com/black-sliver/PopTracker/releases) +

What the Mod Manager Should Look Like.

+ ![image](https://i.imgur.com/Si4oZ8w.png) +

Using the KH2 Client

-Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
+Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
When you successfully connect to the server the client will automatically hook into the game to send/receive checks.
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.
`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`
Most checks will be sent to you anywhere outside a load or cutscene.
`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.` -
+

KH2 Client should look like this:

+ ![image](https://i.imgur.com/qP6CmV8.png) -
-Enter `The room's port number` into the top box where the x's are and press "Connect". Follow the prompts there and you should be connected +Enter `The room's port number` into the top box where the x's are and press "Connect". Follow the prompts there and you should be connected

Common Pitfalls

+ - Having an old GOA Lua Script in your `C:\Users\*YourName*\Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\kh2` folder. - - Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png) -
+ - Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png) - Not having Lua Backend Configured Correctly. - - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Lua Backend Configuration Step. -
-- Loading into Simulated Twilight Town Instead of the GOA. - - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Lua Backend Configuration Step. +- Loading into Simulated Twilight Town Instead of the GOA. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.

Best Practices

@@ -69,9 +79,26 @@ Enter `The room's port number` into the top box where the x's are and pr - Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. - Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed -

Logic Sheet

+

Logic Sheet & PopTracker Autotracking

+ Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing) + +Alternatively you can use the Kingdom Hearts 2 PopTracker Pack that is based off of the logic sheet above and does all the work for you. + +

PopTracker Pack

+ +1. Download [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/) and +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Put the tracker pack into packs/ in your PopTracker install. +3. Open PopTracker, and load the Kingdom Hearts 2 pack. +4. For autotracking, click on the "AP" symbol at the top. +5. Enter the Archipelago server address (the one you connected your client to), slot name, and password. + +This pack will handle logic, received items, checked locations and autotabbing for you! + +

F.A.Q.

+ - Why is my Client giving me a "Cannot Open Process: " error? - Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin. - Why is my HP/MP continuously increasing without stopping? @@ -83,11 +110,13 @@ Have any questions on what's in logic? This spreadsheet made by Bulcon has the a - Why did I not load into the correct visit? - You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item. - What versions of Kingdom Hearts 2 are supported? - - Currently `only` the most up to date version on the Epic Game Store is supported: version `1.0.0.8_WW`. + - Currently the `only` supported versions are `Epic Games Version 1.0.0.9_WW` and `Steam Build Version 14716933`. - Why am I getting wallpapered while going into a world for the first time? - - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. + - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. - Why am I not getting magic? - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +- Why did I crash after picking my dream weapon? + - This is normally caused by having an outdated GOA mod or having an outdated panacea and/or luabackend. To fix this rerun the setup wizard and reinstall luabackend and panacea. Also make sure all your mods are up-to-date. - Why did I crash? - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify @@ -99,5 +128,3 @@ Have any questions on what's in logic? This spreadsheet made by Bulcon has the a - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. - How do I load an auto save? - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. - - diff --git a/worlds/ladx/ItemIconGuessing.py b/worlds/ladx/ItemIconGuessing.py new file mode 100644 index 000000000000..e3d2ad7b8295 --- /dev/null +++ b/worlds/ladx/ItemIconGuessing.py @@ -0,0 +1,531 @@ +BLOCKED_ASSOCIATIONS = [ + # MAX_ARROWS_UPGRADE, MAX_BOMBS_UPGRADE, MAX_POWDER_UPGRADE + # arrows and bombs will be matched to arrow and bomb respectively through pluralization + "ARROWS", + "BOMBS", + "MAX", + "UPGRADE", + + "TAIL", # TAIL_KEY + "ANGLER", # ANGLER_KEY + "FACE", # FACE_KEY + "BIRD", # BIRD_KEY + "SLIME", # SLIME_KEY + "NIGHTMARE",# NIGHTMARE_KEY + + "BLUE", # BLUE_TUNIC + "RED", # RED_TUNIC + + "TRADING", # TRADING_ITEM_* + "ITEM", # TRADING_ITEM_* + + "BAD", # BAD_HEART_CONTAINER + "GOLD", # GOLD_LEAF + "MAGIC", # MAGIC_POWDER, MAGIC_ROD + "MESSAGE", # MESSAGE (Master Stalfos' Message) + "PEGASUS", # PEGASUS_BOOTS + "PIECE", # HEART_PIECE, PIECE_OF_POWER + "POWER", # POWER_BRACELET, PIECE_OF_POWER + "SINGLE", # SINGLE_ARROW + "STONE", # STONE_BEAK + + "BEAK1", + "BEAK2", + "BEAK3", + "BEAK4", + "BEAK5", + "BEAK6", + "BEAK7", + "BEAK8", + + "COMPASS1", + "COMPASS2", + "COMPASS3", + "COMPASS4", + "COMPASS5", + "COMPASS6", + "COMPASS7", + "COMPASS8", + + "MAP1", + "MAP2", + "MAP3", + "MAP4", + "MAP5", + "MAP6", + "MAP7", + "MAP8", +] + +# Single word synonyms for Link's Awakening items, for generic matching. +SYNONYMS = { + # POWER_BRACELET + 'ANKLET': 'POWER_BRACELET', + 'ARMLET': 'POWER_BRACELET', + 'BAND': 'POWER_BRACELET', + 'BANGLE': 'POWER_BRACELET', + 'BRACER': 'POWER_BRACELET', + 'CARRY': 'POWER_BRACELET', + 'CIRCLET': 'POWER_BRACELET', + 'CROISSANT': 'POWER_BRACELET', + 'GAUNTLET': 'POWER_BRACELET', + 'GLOVE': 'POWER_BRACELET', + 'RING': 'POWER_BRACELET', + 'STRENGTH': 'POWER_BRACELET', + + # SHIELD + 'AEGIS': 'SHIELD', + 'BUCKLER': 'SHIELD', + 'SHLD': 'SHIELD', + + # BOW + 'BALLISTA': 'BOW', + + # HOOKSHOT + 'GRAPPLE': 'HOOKSHOT', + 'GRAPPLING': 'HOOKSHOT', + 'ROPE': 'HOOKSHOT', + + # MAGIC_ROD + 'BEAM': 'MAGIC_ROD', + 'CANE': 'MAGIC_ROD', + 'STAFF': 'MAGIC_ROD', + 'WAND': 'MAGIC_ROD', + + # PEGASUS_BOOTS + 'BOOT': 'PEGASUS_BOOTS', + 'GREAVES': 'PEGASUS_BOOTS', + 'RUN': 'PEGASUS_BOOTS', + 'SHOE': 'PEGASUS_BOOTS', + 'SPEED': 'PEGASUS_BOOTS', + + # OCARINA + 'FLUTE': 'OCARINA', + 'RECORDER': 'OCARINA', + + # FEATHER + 'JUMP': 'FEATHER', + 'PLUME': 'FEATHER', + 'WING': 'FEATHER', + + # SHOVEL + 'DIG': 'SHOVEL', + + # MAGIC_POWDER + 'BAG': 'MAGIC_POWDER', + 'CASE': 'MAGIC_POWDER', + 'DUST': 'MAGIC_POWDER', + 'POUCH': 'MAGIC_POWDER', + 'SACK': 'MAGIC_POWDER', + + # BOMB + 'BLAST': 'BOMB', + 'BOMBCHU': 'BOMB', + 'FIRECRACKER': 'BOMB', + 'TNT': 'BOMB', + + # SWORD + 'BLADE': 'SWORD', + 'CUT': 'SWORD', + 'DAGGER': 'SWORD', + 'DIRK': 'SWORD', + 'EDGE': 'SWORD', + 'EPEE': 'SWORD', + 'EXCALIBUR': 'SWORD', + 'FALCHION': 'SWORD', + 'KATANA': 'SWORD', + 'KNIFE': 'SWORD', + 'MACHETE': 'SWORD', + 'MASAMUNE': 'SWORD', + 'MURASAME': 'SWORD', + 'SABER': 'SWORD', + 'SABRE': 'SWORD', + 'SCIMITAR': 'SWORD', + 'SLASH': 'SWORD', + + # FLIPPERS + 'FLIPPER': 'FLIPPERS', + 'SWIM': 'FLIPPERS', + + # MEDICINE + 'BOTTLE': 'MEDICINE', + 'FLASK': 'MEDICINE', + 'LEMONADE': 'MEDICINE', + 'POTION': 'MEDICINE', + 'TEA': 'MEDICINE', + + # TAIL_KEY + + # ANGLER_KEY + + # FACE_KEY + + # BIRD_KEY + + # SLIME_KEY + + # GOLD_LEAF + 'HERB': 'GOLD_LEAF', + + # RUPEES_20 + 'COIN': 'RUPEES_20', + 'MONEY': 'RUPEES_20', + 'RUPEE': 'RUPEES_20', + + # RUPEES_50 + + # RUPEES_100 + + # RUPEES_200 + + # RUPEES_500 + 'GEM': 'RUPEES_500', + 'JEWEL': 'RUPEES_500', + + # SEASHELL + 'CARAPACE': 'SEASHELL', + 'CONCH': 'SEASHELL', + 'SHELL': 'SEASHELL', + + # MESSAGE (master stalfos message) + 'NOTHING': 'MESSAGE', + 'TRAP': 'MESSAGE', + + # BOOMERANG + 'BOOMER': 'BOOMERANG', + + # HEART_PIECE + + # BOWWOW + 'BEAST': 'BOWWOW', + 'PET': 'BOWWOW', + + # ARROWS_10 + + # SINGLE_ARROW + 'MISSILE': 'SINGLE_ARROW', + 'QUIVER': 'SINGLE_ARROW', + + # ROOSTER + 'BIRD': 'ROOSTER', + 'CHICKEN': 'ROOSTER', + 'CUCCO': 'ROOSTER', + 'FLY': 'ROOSTER', + 'GRIFFIN': 'ROOSTER', + 'GRYPHON': 'ROOSTER', + + # MAX_POWDER_UPGRADE + + # MAX_BOMBS_UPGRADE + + # MAX_ARROWS_UPGRADE + + # RED_TUNIC + + # BLUE_TUNIC + 'ARMOR': 'BLUE_TUNIC', + 'MAIL': 'BLUE_TUNIC', + 'SUIT': 'BLUE_TUNIC', + + # HEART_CONTAINER + 'TANK': 'HEART_CONTAINER', + + # TOADSTOOL + 'FUNGAL': 'TOADSTOOL', + 'FUNGUS': 'TOADSTOOL', + 'MUSHROOM': 'TOADSTOOL', + 'SHROOM': 'TOADSTOOL', + + # GUARDIAN_ACORN + 'NUT': 'GUARDIAN_ACORN', + 'SEED': 'GUARDIAN_ACORN', + + # KEY + 'DOOR': 'KEY', + 'GATE': 'KEY', + 'KEY': 'KEY', # Without this, foreign keys show up as nightmare keys + 'LOCK': 'KEY', + 'PANEL': 'KEY', + 'UNLOCK': 'KEY', + + # NIGHTMARE_KEY + + # MAP + + # COMPASS + + # STONE_BEAK + 'FOSSIL': 'STONE_BEAK', + 'RELIC': 'STONE_BEAK', + + # SONG1 + 'BOLERO': 'SONG1', + 'LULLABY': 'SONG1', + 'MELODY': 'SONG1', + 'MINUET': 'SONG1', + 'NOCTURNE': 'SONG1', + 'PRELUDE': 'SONG1', + 'REQUIEM': 'SONG1', + 'SERENADE': 'SONG1', + 'SONG': 'SONG1', + + # SONG2 + 'FISH': 'SONG2', + 'SURF': 'SONG2', + + # SONG3 + 'FROG': 'SONG3', + + # INSTRUMENT1 + 'CELLO': 'INSTRUMENT1', + 'GUITAR': 'INSTRUMENT1', + 'LUTE': 'INSTRUMENT1', + 'VIOLIN': 'INSTRUMENT1', + + # INSTRUMENT2 + 'HORN': 'INSTRUMENT2', + + # INSTRUMENT3 + 'BELL': 'INSTRUMENT3', + 'CHIME': 'INSTRUMENT3', + + # INSTRUMENT4 + 'HARP': 'INSTRUMENT4', + 'KANTELE': 'INSTRUMENT4', + + # INSTRUMENT5 + 'MARIMBA': 'INSTRUMENT5', + 'XYLOPHONE': 'INSTRUMENT5', + + # INSTRUMENT6 (triangle) + + # INSTRUMENT7 + 'KEYBOARD': 'INSTRUMENT7', + 'ORGAN': 'INSTRUMENT7', + 'PIANO': 'INSTRUMENT7', + + # INSTRUMENT8 + 'DRUM': 'INSTRUMENT8', + + # TRADING_ITEM_YOSHI_DOLL + 'DINOSAUR': 'TRADING_ITEM_YOSHI_DOLL', + 'DRAGON': 'TRADING_ITEM_YOSHI_DOLL', + 'TOY': 'TRADING_ITEM_YOSHI_DOLL', + + # TRADING_ITEM_RIBBON + 'HAIRBAND': 'TRADING_ITEM_RIBBON', + 'HAIRPIN': 'TRADING_ITEM_RIBBON', + + # TRADING_ITEM_DOG_FOOD + 'CAN': 'TRADING_ITEM_DOG_FOOD', + + # TRADING_ITEM_BANANAS + 'BANANA': 'TRADING_ITEM_BANANAS', + + # TRADING_ITEM_STICK + 'BRANCH': 'TRADING_ITEM_STICK', + 'TWIG': 'TRADING_ITEM_STICK', + + # TRADING_ITEM_HONEYCOMB + 'BEEHIVE': 'TRADING_ITEM_HONEYCOMB', + 'HIVE': 'TRADING_ITEM_HONEYCOMB', + 'HONEY': 'TRADING_ITEM_HONEYCOMB', + + # TRADING_ITEM_PINEAPPLE + 'FOOD': 'TRADING_ITEM_PINEAPPLE', + 'FRUIT': 'TRADING_ITEM_PINEAPPLE', + 'GOURD': 'TRADING_ITEM_PINEAPPLE', + + # TRADING_ITEM_HIBISCUS + 'FLOWER': 'TRADING_ITEM_HIBISCUS', + 'PETAL': 'TRADING_ITEM_HIBISCUS', + + # TRADING_ITEM_LETTER + 'CARD': 'TRADING_ITEM_LETTER', + 'MESSAGE': 'TRADING_ITEM_LETTER', + + # TRADING_ITEM_BROOM + 'SWEEP': 'TRADING_ITEM_BROOM', + + # TRADING_ITEM_FISHING_HOOK + 'CLAW': 'TRADING_ITEM_FISHING_HOOK', + + # TRADING_ITEM_NECKLACE + 'AMULET': 'TRADING_ITEM_NECKLACE', + 'BEADS': 'TRADING_ITEM_NECKLACE', + 'PEARLS': 'TRADING_ITEM_NECKLACE', + 'PENDANT': 'TRADING_ITEM_NECKLACE', + 'ROSARY': 'TRADING_ITEM_NECKLACE', + + # TRADING_ITEM_SCALE + + # TRADING_ITEM_MAGNIFYING_GLASS + 'FINDER': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'LENS': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'SCOPE': 'TRADING_ITEM_MAGNIFYING_GLASS', + 'XRAY': 'TRADING_ITEM_MAGNIFYING_GLASS', + + # PIECE_OF_POWER + 'TRIANGLE': 'PIECE_OF_POWER', + 'POWER': 'PIECE_OF_POWER', + 'TRIFORCE': 'PIECE_OF_POWER', +} + +# For generic multi-word matches. +PHRASES = { + 'BIG KEY': 'NIGHTMARE_KEY', + 'BOSS KEY': 'NIGHTMARE_KEY', + 'HEART PIECE': 'HEART_PIECE', + 'PIECE OF HEART': 'HEART_PIECE', +} + +# All following will only be used to match items for the specific game. +# Item names will be uppercased when comparing. +# Can be multi-word. +GAME_SPECIFIC_PHRASES = { + 'Final Fantasy': { + 'OXYALE': 'MEDICINE', + 'VORPAL': 'SWORD', + 'XCALBER': 'SWORD', + }, + + 'The Legend of Zelda': { + 'WATER OF LIFE': 'MEDICINE', + }, + + 'The Legend of Zelda - Oracle of Seasons': { + 'RARE PEACH STONE': 'HEART_PIECE', + }, + + 'Noita': { + 'ALL-SEEING EYE': 'TRADING_ITEM_MAGNIFYING_GLASS', # lets you find secrets + }, + + 'Ocarina of Time': { + 'COJIRO': 'ROOSTER', + }, + + 'SMZ3': { + 'BIGKEY': 'NIGHTMARE_KEY', + 'BYRNA': 'MAGIC_ROD', + 'HEARTPIECE': 'HEART_PIECE', + 'POWERBOMB': 'BOMB', + 'SOMARIA': 'MAGIC_ROD', + 'SUPER': 'SINGLE_ARROW', + }, + + 'Sonic Adventure 2 Battle': { + 'CHAOS EMERALD': 'PIECE_OF_POWER', + }, + + 'Super Mario 64': { + 'POWER STAR': 'PIECE_OF_POWER', + }, + + 'Super Mario World': { + 'P-BALLOON': 'FEATHER', + }, + + 'Super Metroid': { + 'POWER BOMB': 'BOMB', + }, + + 'The Witness': { + 'BONK': 'BOMB', + 'BUNKER LASER': 'INSTRUMENT4', + 'DESERT LASER': 'INSTRUMENT5', + 'JUNGLE LASER': 'INSTRUMENT4', + 'KEEP LASER': 'INSTRUMENT7', + 'MONASTERY LASER': 'INSTRUMENT1', + 'POWER SURGE': 'BOMB', + 'PUZZLE SKIP': 'GOLD_LEAF', + 'QUARRY LASER': 'INSTRUMENT8', + 'SHADOWS LASER': 'INSTRUMENT1', + 'SHORTCUTS': 'KEY', + 'SLOWNESS': 'BOMB', + 'SWAMP LASER': 'INSTRUMENT2', + 'SYMMETRY LASER': 'INSTRUMENT6', + 'TOWN LASER': 'INSTRUMENT3', + 'TREEHOUSE LASER': 'INSTRUMENT2', + 'WATER PUMPS': 'KEY', + }, + + 'TUNIC': { + "AURA'S GEM": 'SHIELD', # card that enhances the shield + 'DUSTY': 'TRADING_ITEM_BROOM', # a broom + 'HERO RELIC - HP': 'TRADING_ITEM_HIBISCUS', + 'HERO RELIC - MP': 'TOADSTOOL', + 'HERO RELIC - SP': 'FEATHER', + 'HP BERRY': 'GUARDIAN_ACORN', + 'HP OFFERING': 'TRADING_ITEM_HIBISCUS', # a flower + 'LUCKY CUP': 'HEART_CONTAINER', # card with a heart on it + 'INVERTED ASH': 'MEDICINE', # card with a potion on it + 'MAGIC ORB': 'HOOKSHOT', + 'MP BERRY': 'GUARDIAN_ACORN', + 'MP OFFERING': 'TOADSTOOL', # a mushroom + 'QUESTAGON': 'PIECE_OF_POWER', # triforce piece equivalent + 'SP OFFERING': 'FEATHER', # a feather + 'SPRING FALLS': 'TRADING_ITEM_HIBISCUS', # a flower + }, + + 'FNaFW': { + 'Freddy': 'TRADING_ITEM_YOSHI_DOLL', # all of these are animatronics, aka dolls. + 'Bonnie': 'TRADING_ITEM_YOSHI_DOLL', + 'Chica': 'TRADING_ITEM_YOSHI_DOLL', + 'Foxy': 'TRADING_ITEM_YOSHI_DOLL', + 'Toy Bonnie': 'TRADING_ITEM_YOSHI_DOLL', + 'Toy Chica': 'TRADING_ITEM_YOSHI_DOLL', + 'Toy Freddy': 'TRADING_ITEM_YOSHI_DOLL', + 'Mangle': 'TRADING_ITEM_YOSHI_DOLL', + 'Balloon Boy': 'TRADING_ITEM_YOSHI_DOLL', + 'JJ': 'TRADING_ITEM_YOSHI_DOLL', + 'Phantom Freddy': 'TRADING_ITEM_YOSHI_DOLL', + 'Phantom BB': 'TRADING_ITEM_YOSHI_DOLL', + 'Phantom Chica': 'TRADING_ITEM_YOSHI_DOLL', + 'Phantom Mangle': 'TRADING_ITEM_YOSHI_DOLL', + 'Withered Foxy': 'TRADING_ITEM_YOSHI_DOLL', + 'Phantom Foxy': 'TRADING_ITEM_YOSHI_DOLL', + 'Withered Chica': 'TRADING_ITEM_YOSHI_DOLL', + 'Withered Freddy': 'TRADING_ITEM_YOSHI_DOLL', + 'Withered Bonnie': 'TRADING_ITEM_YOSHI_DOLL', + 'Shadow Freddy': 'TRADING_ITEM_YOSHI_DOLL', + 'Marionette': 'TRADING_ITEM_YOSHI_DOLL', + 'Phantom Marionette': 'TRADING_ITEM_YOSHI_DOLL', + 'Golden Freddy': 'TRADING_ITEM_YOSHI_DOLL', + 'Paperpals': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmare Freddy': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmare Bonnie': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmare Chica': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmare Foxy': 'TRADING_ITEM_YOSHI_DOLL', + 'Endo 01': 'TRADING_ITEM_YOSHI_DOLL', + 'Endo 02': 'TRADING_ITEM_YOSHI_DOLL', + 'Plushtrap': 'TRADING_ITEM_YOSHI_DOLL', + 'Endoplush': 'TRADING_ITEM_YOSHI_DOLL', + 'Springtrap': 'TRADING_ITEM_YOSHI_DOLL', + 'RWQFSFASXC': 'TRADING_ITEM_YOSHI_DOLL', + 'Crying Child': 'TRADING_ITEM_YOSHI_DOLL', + 'Funtime Foxy': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmare Fredbear': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmare': 'TRADING_ITEM_YOSHI_DOLL', + 'Fredbear': 'TRADING_ITEM_YOSHI_DOLL', + 'Spring Bonnie': 'TRADING_ITEM_YOSHI_DOLL', + 'Jack-O-Chica': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmare BB': 'TRADING_ITEM_YOSHI_DOLL', + 'Coffee': 'TRADING_ITEM_YOSHI_DOLL', + 'Jack-O-Bonnie': 'TRADING_ITEM_YOSHI_DOLL', + 'Purpleguy': 'TRADING_ITEM_YOSHI_DOLL', + 'Nightmarionne': 'TRADING_ITEM_YOSHI_DOLL', + 'Mr. Chipper': 'TRADING_ITEM_YOSHI_DOLL', + 'Animdude': 'TRADING_ITEM_YOSHI_DOLL', + 'Progressive Endoskeleton': 'BLUE_TUNIC', # basically armor you wear to give you more defense + '25 Tokens': 'RUPEES_20', # money + '50 Tokens': 'RUPEES_50', + '100 Tokens': 'RUPEES_100', + '250 Tokens': 'RUPEES_200', + '500 Tokens': 'RUPEES_500', + '1000 Tokens': 'RUPEES_500', + '2500 Tokens': 'RUPEES_500', + '5000 Tokens': 'RUPEES_500', + }, +} diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index 9f4784f74995..32d466373cae 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -26,7 +26,7 @@ class DungeonItemData(ItemData): @property def dungeon_index(self): return int(self.ladxr_id[-1]) - + @property def dungeon_item_type(self): s = self.ladxr_id[:-1] @@ -69,7 +69,6 @@ class ItemName: BOMB = "Bomb" SWORD = "Progressive Sword" FLIPPERS = "Flippers" - MAGNIFYING_LENS = "Magnifying Lens" MEDICINE = "Medicine" TAIL_KEY = "Tail Key" ANGLER_KEY = "Angler Key" @@ -83,8 +82,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" @@ -99,6 +98,7 @@ class ItemName: HEART_CONTAINER = "Heart Container" BAD_HEART_CONTAINER = "Bad Heart Container" TOADSTOOL = "Toadstool" + GUARDIAN_ACORN = "Guardian Acorn" KEY = "Key" KEY1 = "Small Key (Tail Cave)" KEY2 = "Small Key (Bottle Grotto)" @@ -174,8 +174,9 @@ class ItemName: TRADING_ITEM_NECKLACE = "Necklace" TRADING_ITEM_SCALE = "Scale" TRADING_ITEM_MAGNIFYING_GLASS = "Magnifying Glass" + PIECE_OF_POWER = "Piece Of Power" -trade_item_prog = ItemClassification.progression +trade_item_prog = ItemClassification.progression links_awakening_items = [ ItemData(ItemName.POWER_BRACELET, "POWER_BRACELET", ItemClassification.progression), @@ -191,7 +192,6 @@ class ItemName: ItemData(ItemName.BOMB, "BOMB", ItemClassification.progression), ItemData(ItemName.SWORD, "SWORD", ItemClassification.progression), ItemData(ItemName.FLIPPERS, "FLIPPERS", ItemClassification.progression), - ItemData(ItemName.MAGNIFYING_LENS, "MAGNIFYING_LENS", ItemClassification.progression), ItemData(ItemName.MEDICINE, "MEDICINE", ItemClassification.useful), ItemData(ItemName.TAIL_KEY, "TAIL_KEY", ItemClassification.progression), ItemData(ItemName.ANGLER_KEY, "ANGLER_KEY", ItemClassification.progression), @@ -221,6 +221,7 @@ class ItemName: ItemData(ItemName.HEART_CONTAINER, "HEART_CONTAINER", ItemClassification.useful), #ItemData(ItemName.BAD_HEART_CONTAINER, "BAD_HEART_CONTAINER", ItemClassification.trap), ItemData(ItemName.TOADSTOOL, "TOADSTOOL", ItemClassification.progression), + ItemData(ItemName.GUARDIAN_ACORN, "GUARDIAN_ACORN", ItemClassification.filler), DungeonItemData(ItemName.KEY, "KEY", ItemClassification.progression), DungeonItemData(ItemName.KEY1, "KEY1", ItemClassification.progression), DungeonItemData(ItemName.KEY2, "KEY2", ItemClassification.progression), @@ -295,7 +296,8 @@ class ItemName: TradeItemData(ItemName.TRADING_ITEM_FISHING_HOOK, "TRADING_ITEM_FISHING_HOOK", trade_item_prog, "Grandma (Animal Village)"), TradeItemData(ItemName.TRADING_ITEM_NECKLACE, "TRADING_ITEM_NECKLACE", trade_item_prog, "Fisher (Martha's Bay)"), TradeItemData(ItemName.TRADING_ITEM_SCALE, "TRADING_ITEM_SCALE", trade_item_prog, "Mermaid (Martha's Bay)"), - TradeItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog, "Mermaid Statue (Martha's Bay)") + TradeItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog, "Mermaid Statue (Martha's Bay)"), + ItemData(ItemName.PIECE_OF_POWER, "PIECE_OF_POWER", ItemClassification.filler), ] ladxr_item_to_la_item_name = { @@ -305,3 +307,135 @@ class ItemName: links_awakening_items_by_name = { item.item_name : item for item in links_awakening_items } + +links_awakening_item_name_groups: typing.Dict[str, typing.Set[str]] = { + "Instruments": { + "Full Moon Cello", + "Conch Horn", + "Sea Lily's Bell", + "Surf Harp", + "Wind Marimba", + "Coral Triangle", + "Organ of Evening Calm", + "Thunder Drum", + }, + "Entrance Keys": { + "Tail Key", + "Angler Key", + "Face Key", + "Bird Key", + "Slime Key", + }, + "Nightmare Keys": { + "Nightmare Key (Angler's Tunnel)", + "Nightmare Key (Bottle Grotto)", + "Nightmare Key (Catfish's Maw)", + "Nightmare Key (Color Dungeon)", + "Nightmare Key (Eagle's Tower)", + "Nightmare Key (Face Shrine)", + "Nightmare Key (Key Cavern)", + "Nightmare Key (Tail Cave)", + "Nightmare Key (Turtle Rock)", + }, + "Small Keys": { + "Small Key (Angler's Tunnel)", + "Small Key (Bottle Grotto)", + "Small Key (Catfish's Maw)", + "Small Key (Color Dungeon)", + "Small Key (Eagle's Tower)", + "Small Key (Face Shrine)", + "Small Key (Key Cavern)", + "Small Key (Tail Cave)", + "Small Key (Turtle Rock)", + }, + "Compasses": { + "Compass (Angler's Tunnel)", + "Compass (Bottle Grotto)", + "Compass (Catfish's Maw)", + "Compass (Color Dungeon)", + "Compass (Eagle's Tower)", + "Compass (Face Shrine)", + "Compass (Key Cavern)", + "Compass (Tail Cave)", + "Compass (Turtle Rock)", + }, + "Maps": { + "Dungeon Map (Angler's Tunnel)", + "Dungeon Map (Bottle Grotto)", + "Dungeon Map (Catfish's Maw)", + "Dungeon Map (Color Dungeon)", + "Dungeon Map (Eagle's Tower)", + "Dungeon Map (Face Shrine)", + "Dungeon Map (Key Cavern)", + "Dungeon Map (Tail Cave)", + "Dungeon Map (Turtle Rock)", + }, + "Stone Beaks": { + "Stone Beak (Angler's Tunnel)", + "Stone Beak (Bottle Grotto)", + "Stone Beak (Catfish's Maw)", + "Stone Beak (Color Dungeon)", + "Stone Beak (Eagle's Tower)", + "Stone Beak (Face Shrine)", + "Stone Beak (Key Cavern)", + "Stone Beak (Tail Cave)", + "Stone Beak (Turtle Rock)", + }, + "Trading Items": { + "Yoshi Doll", + "Ribbon", + "Dog Food", + "Bananas", + "Stick", + "Honeycomb", + "Pineapple", + "Hibiscus", + "Letter", + "Broom", + "Fishing Hook", + "Necklace", + "Scale", + "Magnifying Glass", + }, + "Rupees": { + "20 Rupees", + "50 Rupees", + "100 Rupees", + "200 Rupees", + "500 Rupees", + }, + "Upgrades": { + "Max Powder Upgrade", + "Max Bombs Upgrade", + "Max Arrows Upgrade", + }, + "Songs": { + "Ballad of the Wind Fish", + "Manbo's Mambo", + "Frog's Song of Soul", + }, + "Tunics": { + "Red Tunic", + "Blue Tunic", + }, + "Bush Breakers": { + "Progressive Power Bracelet", + "Magic Rod", + "Magic Powder", + "Bomb", + "Progressive Sword", + "Boomerang", + }, + "Sword": { + "Progressive Sword", + }, + "Shield": { + "Progressive Shield", + }, + "Power Bracelet": { + "Progressive Power Bracelet", + }, + "Bracelet": { + "Progressive Power Bracelet", + }, +} diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index e6f608a92180..046b51815cba 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -58,7 +58,7 @@ from .patches import bank34 from .utils import formatText -from ..Options import TrendyGame, Palette +from ..Options import TrendyGame, Palette, Warps from .roomEditor import RoomEditor, Object from .patches.aesthetics import rgb_to_bin, bin_to_rgb @@ -153,7 +153,9 @@ def generateRom(args, world: "LinksAwakeningWorld"): if world.ladxr_settings.witch: patches.witch.updateWitch(rom) patches.softlock.fixAll(rom) - patches.maptweaks.tweakMap(rom) + if not world.ladxr_settings.rooster: + patches.maptweaks.tweakMap(rom) + patches.maptweaks.tweakBirdKeyRoom(rom) patches.chest.fixChests(rom) patches.shop.fixShop(rom) patches.rooster.patchRooster(rom) @@ -176,11 +178,7 @@ def generateRom(args, world: "LinksAwakeningWorld"): patches.songs.upgradeMarin(rom) patches.songs.upgradeManbo(rom) patches.songs.upgradeMamu(rom) - if world.ladxr_settings.tradequest: - patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings.boomerang) - else: - # Monkey bridge patch, always have the bridge there. - rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) + patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings) patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal') if world.ladxr_settings.bowwow != 'normal': patches.bowwow.bowwowMapPatches(rom) @@ -268,6 +266,8 @@ def generateRom(args, world: "LinksAwakeningWorld"): our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification] def gen_hint(): + if not world.options.in_game_hints: + return 'Hints are disabled!' chance = world.random.uniform(0, 1) if chance < JUNK_HINT: return None @@ -280,15 +280,19 @@ 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 else: location_name = location.name - hint = f"{name} {location.item} is at {location_name}" + hint = f"{name} {location.item.name} 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 @@ -338,11 +342,53 @@ def gen_hint(): patches.enemies.doubleTrouble(rom) if world.options.text_shuffle: + excluded_ids = [ + # Overworld owl statues + 0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D, + + # Dungeon owls + 0x288, 0x280, # D1 + 0x28A, 0x289, 0x281, # D2 + 0x282, 0x28C, 0x28B, # D3 + 0x283, # D4 + 0x28D, 0x284, # D5 + 0x285, 0x28F, 0x28E, # D6 + 0x291, 0x290, 0x286, # D7 + 0x293, 0x287, 0x292, # D8 + 0x263, # D0 + + # Hint books + 0x267, # color dungeon + 0x200, 0x201, + 0x202, 0x203, + 0x204, 0x205, + 0x206, 0x207, + 0x208, 0x209, + 0x20A, 0x20B, + 0x20C, + 0x20D, 0x20E, + 0x217, 0x218, 0x219, 0x21A, + + # Goal sign + 0x1A3, + + # Signpost maze + 0x1A9, 0x1AA, 0x1AB, 0x1AC, 0x1AD, + + # Prices + 0x02C, 0x02D, 0x030, 0x031, 0x032, 0x033, # Shop items + 0x03B, # Trendy Game + 0x045, # Fisherman + 0x018, 0x019, # Crazy Tracy + 0x0DC, # Mamu + 0x0F0, # Raft ride + ] + excluded_texts = [ rom.texts[excluded_id] for excluded_id in excluded_ids] buckets = defaultdict(list) # For each ROM bank, shuffle text within the bank for n, data in enumerate(rom.texts._PointerTable__data): # Don't muck up which text boxes are questions and which are statements - if type(data) != int and data and data != b'\xFF': + if type(data) != int and data and data != b'\xFF' and data not in excluded_texts: buckets[(rom.texts._PointerTable__banks[n], data[len(data) - 1] == 0xfe)].append((n, data)) for bucket in buckets.values(): # For each bucket, make a copy and shuffle @@ -414,8 +460,8 @@ def speed(): for channel in range(3): color[channel] = color[channel] * 31 // 0xbc - if world.options.warp_improvements: - patches.core.addWarpImprovements(rom, world.options.additional_warp_points) + if world.options.warps != Warps.option_vanilla: + patches.core.addWarpImprovements(rom, world.options.warps == Warps.option_improved_additional) palette = world.options.palette if palette != Palette.option_normal: diff --git a/worlds/ladx/LADXR/locations/birdKey.py b/worlds/ladx/LADXR/locations/birdKey.py index 12418c61aa46..0dbdd8653fe2 100644 --- a/worlds/ladx/LADXR/locations/birdKey.py +++ b/worlds/ladx/LADXR/locations/birdKey.py @@ -1,23 +1,6 @@ from .droppedKey import DroppedKey -from ..roomEditor import RoomEditor -from ..assembler import ASM class BirdKey(DroppedKey): def __init__(self): super().__init__(0x27A) - - def patch(self, rom, option, *, multiworld=None): - super().patch(rom, option, multiworld=multiworld) - - re = RoomEditor(rom, self.room) - - # Make the bird key accessible without the rooster - re.removeObject(1, 6) - re.removeObject(2, 6) - re.removeObject(3, 5) - re.removeObject(3, 6) - re.moveObject(1, 5, 2, 6) - re.moveObject(2, 5, 3, 6) - re.addEntity(3, 5, 0x9D) - re.store(rom) diff --git a/worlds/ladx/LADXR/locations/boomerangGuy.py b/worlds/ladx/LADXR/locations/boomerangGuy.py index 92d76cebdf5d..23fcc867617b 100644 --- a/worlds/ladx/LADXR/locations/boomerangGuy.py +++ b/worlds/ladx/LADXR/locations/boomerangGuy.py @@ -24,11 +24,6 @@ def configure(self, options): # But SHIELD, BOMB and MAGIC_POWDER would most likely break things. # SWORD and POWER_BRACELET would most likely introduce the lv0 shield/bracelet issue def patch(self, rom, option, *, multiworld=None): - # Always have the boomerang trade guy enabled (normally you need the magnifier) - rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # show the guy - rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # load the proper room layout - rom.patch(0x19, 0x05F4, ASM("ld a, [wTradeSequenceItem2]\nand a"), ASM("xor a"), fill_nop=True) - if self.setting == 'trade': inv = INVENTORY_MAP[option] # Patch the check if you traded back the boomerang (so traded twice) diff --git a/worlds/ladx/LADXR/locations/constants.py b/worlds/ladx/LADXR/locations/constants.py index 7bb8df5b3515..bcf22711bb7b 100644 --- a/worlds/ladx/LADXR/locations/constants.py +++ b/worlds/ladx/LADXR/locations/constants.py @@ -25,7 +25,7 @@ PEGASUS_BOOTS: 0x05, OCARINA: 0x06, FEATHER: 0x07, SHOVEL: 0x08, MAGIC_POWDER: 0x09, BOMB: 0x0A, SWORD: 0x0B, FLIPPERS: 0x0C, - MAGNIFYING_LENS: 0x0D, MEDICINE: 0x10, + MEDICINE: 0x10, TAIL_KEY: 0x11, ANGLER_KEY: 0x12, FACE_KEY: 0x13, BIRD_KEY: 0x14, GOLD_LEAF: 0x15, RUPEES_50: 0x1B, RUPEES_20: 0x1C, RUPEES_100: 0x1D, RUPEES_200: 0x1E, RUPEES_500: 0x1F, SEASHELL: 0x20, MESSAGE: 0x21, GEL: 0x22, @@ -87,6 +87,8 @@ TOADSTOOL: 0x50, + GUARDIAN_ACORN: 0x51, + HEART_PIECE: 0x80, BOWWOW: 0x81, ARROWS_10: 0x82, @@ -128,4 +130,6 @@ TRADING_ITEM_NECKLACE: 0xA2, TRADING_ITEM_SCALE: 0xA3, TRADING_ITEM_MAGNIFYING_GLASS: 0xA4, + + PIECE_OF_POWER: 0xA5, } diff --git a/worlds/ladx/LADXR/locations/items.py b/worlds/ladx/LADXR/locations/items.py index 50186ef2a34c..56cc52232355 100644 --- a/worlds/ladx/LADXR/locations/items.py +++ b/worlds/ladx/LADXR/locations/items.py @@ -11,7 +11,6 @@ BOMB = "BOMB" SWORD = "SWORD" FLIPPERS = "FLIPPERS" -MAGNIFYING_LENS = "MAGNIFYING_LENS" MEDICINE = "MEDICINE" TAIL_KEY = "TAIL_KEY" ANGLER_KEY = "ANGLER_KEY" @@ -45,6 +44,8 @@ TOADSTOOL = "TOADSTOOL" +GUARDIAN_ACORN = "GUARDIAN_ACORN" + KEY = "KEY" KEY1 = "KEY1" KEY2 = "KEY2" @@ -125,3 +126,5 @@ TRADING_ITEM_NECKLACE = "TRADING_ITEM_NECKLACE" TRADING_ITEM_SCALE = "TRADING_ITEM_SCALE" TRADING_ITEM_MAGNIFYING_GLASS = "TRADING_ITEM_MAGNIFYING_GLASS" + +PIECE_OF_POWER = "PIECE_OF_POWER" \ No newline at end of file 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/logic/dungeon1.py b/worlds/ladx/LADXR/logic/dungeon1.py index 82321a1c0d65..645c50d1d5e5 100644 --- a/worlds/ladx/LADXR/logic/dungeon1.py +++ b/worlds/ladx/LADXR/logic/dungeon1.py @@ -9,7 +9,7 @@ def __init__(self, options, world_setup, r): entrance.add(DungeonChest(0x113), DungeonChest(0x115), DungeonChest(0x10E)) Location(dungeon=1).add(DroppedKey(0x116)).connect(entrance, OR(BOMB, r.push_hardhat)) # hardhat beetles (can kill with bomb) Location(dungeon=1).add(DungeonChest(0x10D)).connect(entrance, OR(r.attack_hookshot_powder, SHIELD)) # moldorm spawn chest - stalfos_keese_room = Location(dungeon=1).add(DungeonChest(0x114)).connect(entrance, r.attack_hookshot) # 2 stalfos 2 keese room + stalfos_keese_room = Location(dungeon=1).add(DungeonChest(0x114)).connect(entrance, AND(OR(r.attack_skeleton, SHIELD),r.attack_hookshot_powder)) # 2 stalfos 2 keese room Location(dungeon=1).add(DungeonChest(0x10C)).connect(entrance, BOMB) # hidden seashell room dungeon1_upper_left = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) if options.owlstatues == "both" or options.owlstatues == "dungeon": @@ -19,21 +19,22 @@ def __init__(self, options, world_setup, r): dungeon1_right_side = Location(dungeon=1).connect(entrance, AND(KEY1, FOUND(KEY1, 3))) if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1) - Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot, SHIELD)) # three of a kind, shield stops the suit from changing + dungeon1_3_of_a_kind = Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot_no_bomb, SHIELD)) # three of a kind, shield stops the suit from changing dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping[0]], FEATHER)) dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1) - Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]]) + boss = Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]]) - if options.logic not in ('normal', 'casual'): + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': stalfos_keese_room.connect(entrance, r.attack_hookshot_powder) # stalfos jump away when you press a button. - + dungeon1_3_of_a_kind.connect(dungeon1_right_side, BOMB) # use timed bombs to match the 3 of a kinds + if options.logic == 'glitched' or options.logic == 'hell': - boss_key.connect(entrance, FEATHER) # super jump + boss_key.connect(entrance, r.super_jump_feather) # super jump dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping[0]]) # damage boost or buffer pause over the pit to cross or mushroom if options.logic == 'hell': feather_chest.connect(dungeon1_upper_left, SWORD) # keep slashing the spiked beetles until they keep moving 1 pixel close towards you and the pit, to get them to fall - boss_key.connect(entrance, FOUND(KEY1,3)) # damage boost off the hardhat to cross the pit + boss_key.connect(entrance, AND(r.damage_boost, FOUND(KEY1,3))) # damage boost off the hardhat to cross the pit self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon2.py b/worlds/ladx/LADXR/logic/dungeon2.py index 3bb95edbc8bd..6ee6cc4a8020 100644 --- a/worlds/ladx/LADXR/logic/dungeon2.py +++ b/worlds/ladx/LADXR/logic/dungeon2.py @@ -14,7 +14,7 @@ def __init__(self, options, world_setup, r): Location(dungeon=2).add(DungeonChest(0x137)).connect(dungeon2_r2, AND(KEY2, FOUND(KEY2, 5), OR(r.rear_attack, r.rear_attack_range))) # compass chest if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=2).add(OwlStatue(0x133)).connect(dungeon2_r2, STONE_BEAK2) - dungeon2_r3 = Location(dungeon=2).add(DungeonChest(0x138)).connect(dungeon2_r2, r.attack_hookshot) # first chest with key, can hookshot the switch in previous room + dungeon2_r3 = Location(dungeon=2).add(DungeonChest(0x138)).connect(dungeon2_r2, r.hit_switch) # first chest with key, can hookshot the switch in previous room dungeon2_r4 = Location(dungeon=2).add(DungeonChest(0x139)).connect(dungeon2_r3, FEATHER) # button spawn chest if options.logic == "casual": shyguy_key_drop = Location(dungeon=2).add(DroppedKey(0x134)).connect(dungeon2_r3, AND(FEATHER, OR(r.rear_attack, r.rear_attack_range))) # shyguy drop key @@ -39,16 +39,16 @@ def __init__(self, options, world_setup, r): if options.logic == 'glitched' or options.logic == 'hell': dungeon2_ghosts_chest.connect(dungeon2_ghosts_room, SWORD) # use sword to spawn ghosts on other side of the room so they run away (logically irrelevant because of torches at start) - dungeon2_r6.connect(miniboss, FEATHER) # superjump to staircase next to hinox. + dungeon2_r6.connect(miniboss, r.super_jump_feather) # superjump to staircase next to hinox. if options.logic == 'hell': - dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, PEGASUS_BOOTS)) # use boots to jump over the pits - dungeon2_r4.connect(dungeon2_r3, OR(PEGASUS_BOOTS, HOOKSHOT)) # can use both pegasus boots bonks or hookshot spam to cross the pit room + dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, r.boots_bonk_pit)) # use boots to jump over the pits + dungeon2_r4.connect(dungeon2_r3, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # can use both pegasus boots bonks or hookshot spam to cross the pit room dungeon2_r4.connect(shyguy_key_drop, r.rear_attack_range, one_way=True) # adjust for alternate requirements for dungeon2_r4 - miniboss.connect(dungeon2_r5, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section + miniboss.connect(dungeon2_r5, AND(r.boots_dash_2d, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section dungeon2_pre_stairs_boss.connect(dungeon2_r6, AND(HOOKSHOT, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1)), FOUND(KEY2, 5))) # hookshot clip through the pot using both pol's voice - dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, AND(PEGASUS_BOOTS, FEATHER))) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic) - dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically + dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, r.boots_jump)) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic) + dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon3.py b/worlds/ladx/LADXR/logic/dungeon3.py index e65c7da0bafc..33782be16c87 100644 --- a/worlds/ladx/LADXR/logic/dungeon3.py +++ b/worlds/ladx/LADXR/logic/dungeon3.py @@ -20,8 +20,8 @@ def __init__(self, options, world_setup, r): Location(dungeon=3).add(OwlStatue(0x154)).connect(area_up, STONE_BEAK3) dungeon3_raised_blocks_north = Location(dungeon=3).add(DungeonChest(0x14C)) # chest locked behind raised blocks near staircase dungeon3_raised_blocks_east = Location(dungeon=3).add(DungeonChest(0x150)) # chest locked behind raised blocks next to slime chest - area_up.connect(dungeon3_raised_blocks_north, r.attack_hookshot, one_way=True) # hit switch to reach north chest - area_up.connect(dungeon3_raised_blocks_east, r.attack_hookshot, one_way=True) # hit switch to reach east chest + area_up.connect(dungeon3_raised_blocks_north, r.hit_switch, one_way=True) # hit switch to reach north chest + area_up.connect(dungeon3_raised_blocks_east, r.hit_switch, one_way=True) # hit switch to reach east chest area_left = Location(dungeon=3).connect(area3, AND(KEY3, FOUND(KEY3, 8))) area_left_key_drop = Location(dungeon=3).add(DroppedKey(0x155)).connect(area_left, r.attack_hookshot) # west key drop (no longer requires feather to get across hole), can use boomerang to knock owls into pit @@ -54,28 +54,30 @@ def __init__(self, options, world_setup, r): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': dungeon3_3_bombite_room.connect(area_right, BOOMERANG) # 3 bombite room from the left side, grab item with boomerang - dungeon3_reverse_eye.connect(entrance, HOOKSHOT) # hookshot the chest to get to the right side - dungeon3_north_key_drop.connect(area_up, POWER_BRACELET) # use pots to kill the enemies - dungeon3_south_key_drop.connect(area_down, POWER_BRACELET) # use pots to kill enemies + dungeon3_reverse_eye.connect(entrance, r.hookshot_over_pit) # hookshot the chest to get to the right side + dungeon3_north_key_drop.connect(area_up, r.throw_pot) # use pots to kill the enemies + dungeon3_south_key_drop.connect(area_down, r.throw_pot) # use pots to kill enemies + area_up.connect(dungeon3_raised_blocks_north, r.throw_pot, one_way=True) # use pots to hit the switch + area_up.connect(dungeon3_raised_blocks_east, AND(r.throw_pot, r.attack_hookshot_powder), one_way=True) # use pots to hit the switch if options.logic == 'glitched' or options.logic == 'hell': - area2.connect(dungeon3_raised_blocks_east, AND(r.attack_hookshot_powder, FEATHER), one_way=True) # use superjump to get over the bottom left block - area3.connect(dungeon3_raised_blocks_north, AND(OR(PEGASUS_BOOTS, HOOKSHOT), FEATHER), one_way=True) # use shagjump (unclipped superjump next to movable block) from north wall to get on the blocks. Instead of boots can also get to that area with a hookshot clip past the movable block - area3.connect(dungeon3_zol_stalfos, HOOKSHOT, one_way=True) # hookshot clip through the northern push block next to raised blocks chest to get to the zol - dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, BOMB)) # superjump to right side 3 gap via top wall and jump the 2 gap - dungeon3_post_dodongo_chest.connect(area_right, AND(FEATHER, FOUND(KEY3, 6))) # superjump from keyblock path. use 2 keys to open enough blocks TODO: nag messages to skip a key + area2.connect(dungeon3_raised_blocks_east, AND(r.attack_hookshot_powder, r.super_jump_feather), one_way=True) # use superjump to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, AND(OR(PEGASUS_BOOTS, r.hookshot_clip_block), r.shaq_jump), one_way=True) # use shagjump (unclipped superjump next to movable block) from north wall to get on the blocks. Instead of boots can also get to that area with a hookshot clip past the movable block + area3.connect(dungeon3_zol_stalfos, r.hookshot_clip_block, one_way=True) # hookshot clip through the northern push block next to raised blocks chest to get to the zol + dungeon3_nightmare_key_chest.connect(area_right, AND(r.super_jump_feather, BOMB)) # superjump to right side 3 gap via top wall and jump the 2 gap + dungeon3_post_dodongo_chest.connect(area_right, AND(r.super_jump_feather, FOUND(KEY3, 6))) # superjump from keyblock path. use 2 keys to open enough blocks TODO: nag messages to skip a key if options.logic == 'hell': - area2.connect(dungeon3_raised_blocks_east, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop to get over the bottom left block - area3.connect(dungeon3_raised_blocks_north, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # use boots superhop off top wall or left wall to get on raised blocks - area_up.connect(dungeon3_zol_stalfos, AND(FEATHER, OR(BOW, MAGIC_ROD, SWORD)), one_way=True) # use superjump near top blocks chest to get to zol without boots, keep wall clip on right wall to get a clip on left wall or use obstacles - area_left_key_drop.connect(area_left, SHIELD) # knock everything into the pit including the teleporting owls - dungeon3_south_key_drop.connect(area_down, SHIELD) # knock everything into the pit including the teleporting owls - dungeon3_nightmare_key_chest.connect(area_right, AND(FEATHER, SHIELD)) # superjump into jumping stalfos and shield bump to right ledge - dungeon3_nightmare_key_chest.connect(area_right, AND(BOMB, PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the pits with pit buffering and hookshot to the chest + area2.connect(dungeon3_raised_blocks_east, r.boots_superhop, one_way=True) # use boots superhop to get over the bottom left block + area3.connect(dungeon3_raised_blocks_north, r.boots_superhop, one_way=True) # use boots superhop off top wall or left wall to get on raised blocks + area_up.connect(dungeon3_zol_stalfos, AND(r.super_jump_feather, r.attack_skeleton), one_way=True) # use superjump near top blocks chest to get to zol without boots, keep wall clip on right wall to get a clip on left wall or use obstacles + area_left_key_drop.connect(area_left, r.shield_bump) # knock everything into the pit including the teleporting owls + dungeon3_south_key_drop.connect(area_down, r.shield_bump) # knock everything into the pit including the teleporting owls + dungeon3_nightmare_key_chest.connect(area_right, AND(r.super_jump_feather, r.shield_bump)) # superjump into jumping stalfos and shield bump to right ledge + dungeon3_nightmare_key_chest.connect(area_right, AND(BOMB, r.pit_buffer_boots, HOOKSHOT)) # boots bonk across the pits with pit buffering and hookshot to the chest compass_chest.connect(dungeon3_3_bombite_room, OR(BOW, MAGIC_ROD, AND(OR(FEATHER, PEGASUS_BOOTS), OR(SWORD, MAGIC_POWDER))), one_way=True) # 3 bombite room from the left side, use a bombite to blow open the wall without bombs pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, FEATHER, POWER_BRACELET)) # use bracelet super bounce glitch to pass through first part underground section - pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, PEGASUS_BOOTS, "MEDICINE2")) # use medicine invulnerability to pass through the 2d section with a boots bonk to reach the staircase + pre_boss.connect(towards_boss4, AND(r.attack_no_boomerang, r.boots_bonk_2d_spikepit)) # use medicine invulnerability to pass through the 2d section with a boots bonk to reach the staircase self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon4.py b/worlds/ladx/LADXR/logic/dungeon4.py index 7d71c89f0c86..a7e06557fa12 100644 --- a/worlds/ladx/LADXR/logic/dungeon4.py +++ b/worlds/ladx/LADXR/logic/dungeon4.py @@ -42,32 +42,36 @@ def __init__(self, options, world_setup, r): boss = Location(dungeon=4).add(HeartContainer(0x166), Instrument(0x162)).connect(before_boss, AND(NIGHTMARE_KEY4, r.boss_requirements[world_setup.boss_mapping[3]])) if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': - sidescroller_key.connect(before_miniboss, AND(FEATHER, BOOMERANG)) # grab the key jumping over the water and boomerang downwards - sidescroller_key.connect(before_miniboss, AND(POWER_BRACELET, FLIPPERS)) # kill the zols with the pots in the room to spawn the key - rightside_crossroads.connect(entrance, FEATHER) # jump across the corners - puddle_crack_block_chest.connect(rightside_crossroads, FEATHER) # jump around the bombable block - north_crossroads.connect(entrance, FEATHER) # jump across the corners - after_double_lock.connect(entrance, FEATHER) # jump across the corners - dungeon4_puddle_before_crossroads.connect(after_double_lock, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers - center_puddle_chest.connect(before_miniboss, FEATHER) # With a tight jump feather is enough to cross the puddle without flippers + sidescroller_key.connect(before_miniboss, BOOMERANG) # fall off the bridge and boomerang downwards before hitting the water to grab the item + sidescroller_key.connect(before_miniboss, AND(r.throw_pot, FLIPPERS)) # kill the zols with the pots in the room to spawn the key + rightside_crossroads.connect(entrance, r.tight_jump) # jump across the corners + puddle_crack_block_chest.connect(rightside_crossroads, r.tight_jump) # jump around the bombable block + north_crossroads.connect(entrance, r.tight_jump) # jump across the corners + after_double_lock.connect(entrance, r.tight_jump) # jump across the corners + dungeon4_puddle_before_crossroads.connect(after_double_lock, r.tight_jump) # With a tight jump feather is enough to cross the puddle without flippers + center_puddle_chest.connect(before_miniboss, r.tight_jump) # With a tight jump feather is enough to cross the puddle without flippers miniboss = Location(dungeon=4).connect(terrace_zols_chest, None, one_way=True) # reach flippers chest through the miniboss room without pulling the lever - to_the_nightmare_key.connect(left_water_area, FEATHER) # With a tight jump feather is enough to reach the top left switch without flippers, or use flippers for puzzle and boots to get through 2d section - before_boss.connect(left_water_area, FEATHER) # jump to the bottom right corner of boss door room + to_the_nightmare_key.connect(left_water_area, r.tight_jump) # With a tight jump feather is enough to reach the top left switch without flippers, or use flippers for puzzle and boots to get through 2d section + before_boss.connect(left_water_area, r.tight_jump) # jump to the bottom right corner of boss door room if options.logic == 'glitched' or options.logic == 'hell': - pushable_block_chest.connect(rightside_crossroads, FLIPPERS) # sideways block push to skip bombs - sidescroller_key.connect(before_miniboss, AND(FEATHER, OR(r.attack_hookshot_powder, POWER_BRACELET))) # superjump into the hole to grab the key while falling into the water - miniboss.connect(before_miniboss, FEATHER) # use jesus jump to transition over the water left of miniboss + pushable_block_chest.connect(rightside_crossroads, AND(r.sideways_block_push, FLIPPERS)) # sideways block push to skip bombs + sidescroller_key.connect(before_miniboss, AND(r.super_jump_feather, OR(r.attack_hookshot_powder, r.throw_pot))) # superjump into the hole to grab the key while falling into the water + miniboss.connect(before_miniboss, r.jesus_jump) # use jesus jump to transition over the water left of miniboss if options.logic == 'hell': - rightside_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into the wall of the first pit, then boots bonk across the center, hookshot to get to the rightmost pit to a second villa buffer on the rightmost pit - pushable_block_chest.connect(rightside_crossroads, OR(PEGASUS_BOOTS, FEATHER)) # use feather to water clip into the top right corner of the bombable block, and sideways block push to gain access. Can boots bonk of top right wall, then water buffer to top of chest and boots bonk to water buffer next to chest - after_double_lock.connect(double_locked_room, AND(FOUND(KEY4, 4), PEGASUS_BOOTS), one_way=True) # use boots bonks to cross the water gaps + rightside_crossroads.connect(entrance, AND(r.pit_buffer_boots, r.hookshot_spam_pit)) # pit buffer into the wall of the first pit, then boots bonk across the center, hookshot to get to the rightmost pit to a second villa buffer on the rightmost pit + rightside_crossroads.connect(after_double_lock, AND(OR(BOMB, BOW), r.hookshot_clip_block)) # split zols for more entities, and clip through the block against the right wall + pushable_block_chest.connect(rightside_crossroads, AND(r.sideways_block_push, OR(r.jesus_buffer, r.jesus_jump))) # use feather to water clip into the top right corner of the bombable block, and sideways block push to gain access. Can boots bonk of top right wall, then water buffer to top of chest and boots bonk to water buffer next to chest + after_double_lock.connect(double_locked_room, AND(FOUND(KEY4, 4), r.pit_buffer_boots), one_way=True) # use boots bonks to cross the water gaps + after_double_lock.connect(entrance, r.pit_buffer_boots) # boots bonk + pit buffer to the bottom + after_double_lock.connect(entrance, AND(r.pit_buffer, r.hookshot_spam_pit)) # hookshot spam over the first pit of crossroads, then buffer down + dungeon4_puddle_before_crossroads.connect(after_double_lock, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk across the water bottom wall to the bottom left corner, then hookshot up north_crossroads.connect(entrance, AND(PEGASUS_BOOTS, HOOKSHOT)) # pit buffer into wall of the first pit, then boots bonk towards the top and hookshot spam to get across (easier with Piece of Power) - after_double_lock.connect(entrance, PEGASUS_BOOTS) # boots bonk + pit buffer to the bottom - dungeon4_puddle_before_crossroads.connect(after_double_lock, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk across the water bottom wall to the bottom left corner, then hookshot up - to_the_nightmare_key.connect(left_water_area, AND(FLIPPERS, PEGASUS_BOOTS)) # Use flippers for puzzle and boots bonk to get through 2d section - before_boss.connect(left_water_area, PEGASUS_BOOTS) # boots bonk across bottom wall then boots bonk to the platform before boss door + before_miniboss.connect(north_crossroads, AND(r.shaq_jump, r.hookshot_clip_block)) # push block left of keyblock up, then shaq jump off the left wall and pause buffer to land on keyblock. + before_miniboss.connect(north_crossroads, AND(OR(BOMB, BOW), r.hookshot_clip_block)) # split zol for more entities, and clip through the block left of keyblock by hookshot spam + to_the_nightmare_key.connect(left_water_area, AND(FLIPPERS, r.boots_bonk)) # use flippers for puzzle and boots bonk to get through 2d section + before_boss.connect(left_water_area, r.pit_buffer_boots) # boots bonk across bottom wall then boots bonk to the platform before boss door self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon5.py b/worlds/ladx/LADXR/logic/dungeon5.py index b8e013066c50..b61e48e255d0 100644 --- a/worlds/ladx/LADXR/logic/dungeon5.py +++ b/worlds/ladx/LADXR/logic/dungeon5.py @@ -39,43 +39,44 @@ def __init__(self, options, world_setup, r): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': blade_trap_chest.connect(area2, AND(FEATHER, r.attack_hookshot_powder)) # jump past the blade traps - boss_key.connect(after_stalfos, AND(FLIPPERS, FEATHER, PEGASUS_BOOTS)) # boots jump across + boss_key.connect(after_stalfos, AND(FLIPPERS, r.boots_jump)) # boots jump across after_stalfos.connect(after_keyblock_boss, AND(FEATHER, r.attack_hookshot_powder)) # circumvent stalfos by going past gohma and backwards from boss door if butterfly_owl: - butterfly_owl.connect(after_stalfos, AND(PEGASUS_BOOTS, STONE_BEAK5)) # boots charge + bonk to cross 2d bridge - after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: boots charge + bonk to cross bridge, past butterfly room and push the block - staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk in 2d section to skip feather - north_of_crossroads.connect(after_stalfos, HOOKSHOT) # hookshot to the right block to cross pits - first_bridge_chest.connect(north_of_crossroads, FEATHER) # tight jump from bottom wall clipped to make it over the pits + butterfly_owl.connect(after_stalfos, AND(r.boots_bonk, STONE_BEAK5)) # boots charge + bonk to cross 2d bridge + after_stalfos.connect(staircase_before_boss, AND(r.boots_bonk, r.attack_hookshot_powder), one_way=True) # pathway from stalfos to staircase: boots charge + bonk to cross bridge, past butterfly room and push the block + staircase_before_boss.connect(post_gohma, AND(r.boots_bonk, HOOKSHOT)) # boots bonk in 2d section to skip feather + north_of_crossroads.connect(after_stalfos, r.hookshot_over_pit) # hookshot to the right block to cross pits + first_bridge_chest.connect(north_of_crossroads, AND(r.wall_clip, r.tight_jump)) # tight jump from bottom wall clipped to make it over the pits after_keyblock_boss.connect(after_stalfos, AND(FEATHER, r.attack_hookshot_powder)) # jump from bottom left to top right, skipping the keyblock - before_boss.connect(after_stalfos, AND(FEATHER, PEGASUS_BOOTS, r.attack_hookshot_powder)) # cross pits room from bottom left to top left with boots jump + before_boss.connect(after_stalfos, AND(r.boots_jump, r.attack_hookshot_powder)) # cross pits room from bottom left to top left with boots jump if options.logic == 'glitched' or options.logic == 'hell': - start_hookshot_chest.connect(entrance, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits + start_hookshot_chest.connect(entrance, r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across the pits post_gohma.connect(area2, HOOKSHOT) # glitch through the blocks/pots with hookshot. Zoomerang can be used but has no logical implications because of 2d section requiring hookshot - north_bridge_chest.connect(north_of_crossroads, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits - east_bridge_chest.connect(first_bridge_chest, FEATHER) # 1 pit buffer to clip bottom wall and jump across the pits - #after_stalfos.connect(staircase_before_boss, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # use the keyblock to get a wall clip in right wall to perform a superjump over the pushable block TODO: nagmessages - after_stalfos.connect(staircase_before_boss, AND(PEGASUS_BOOTS, FEATHER, OR(SWORD, BOW, MAGIC_ROD))) # charge a boots dash in bottom right corner to the right, jump before hitting the wall and use weapon to the left side before hitting the wall + north_bridge_chest.connect(north_of_crossroads, r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across the pits + east_bridge_chest.connect(first_bridge_chest, r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across the pits + #after_stalfos.connect(staircase_before_boss, AND(r.text_clip, r.super_jump)) # use the keyblock to get a wall clip in right wall to perform a superjump over the pushable block + after_stalfos.connect(staircase_before_boss, r.super_jump_boots) # charge a boots dash in bottom right corner to the right, jump before hitting the wall and use weapon to the left side before hitting the wall if options.logic == 'hell': - start_hookshot_chest.connect(entrance, PEGASUS_BOOTS) # use pit buffer to clip into the bottom wall and boots bonk off the wall again - fourth_stalfos_area.connect(compass, AND(PEGASUS_BOOTS, SWORD)) # do an incredibly hard boots bonk setup to get across the hanging platforms in the 2d section - blade_trap_chest.connect(area2, AND(PEGASUS_BOOTS, r.attack_hookshot_powder)) # boots bonk + pit buffer past the blade traps + start_hookshot_chest.connect(entrance, r.pit_buffer_boots) # use pit buffer to clip into the bottom wall and boots bonk off the wall again + fourth_stalfos_area.connect(compass, AND(r.boots_bonk_2d_hell, SWORD)) # do an incredibly hard boots bonk setup to get across the hanging platforms in the 2d section + blade_trap_chest.connect(area2, AND(r.pit_buffer_boots, r.attack_hookshot_powder)) # boots bonk + pit buffer past the blade traps post_gohma.connect(area2, AND(PEGASUS_BOOTS, FEATHER, POWER_BRACELET, r.attack_hookshot_powder)) # use boots jump in room with 2 zols + flying arrows to pit buffer above pot, then jump across. Sideways block push + pick up pots to reach post_gohma - staircase_before_boss.connect(post_gohma, AND(PEGASUS_BOOTS, FEATHER)) # to pass 2d section, tight jump on left screen: hug left wall on little platform, then dash right off platform and jump while in midair to bonk against right wall - after_stalfos.connect(staircase_before_boss, AND(FEATHER, SWORD)) # unclipped superjump in bottom right corner of staircase before boss room, jumping left over the pushable block. reverse is push block + staircase_before_boss.connect(post_gohma, r.boots_jump) # to pass 2d section, tight jump on left screen: hug left wall on little platform, then dash right off platform and jump while in midair to bonk against right wall + after_stalfos.connect(staircase_before_boss, r.super_jump_sword) # unclipped superjump in bottom right corner of staircase before boss room, jumping left over the pushable block. reverse is push block after_stalfos.connect(area2, SWORD) # knock master stalfos down 255 times (about 23 minutes) - north_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering - first_bridge_chest.connect(north_of_crossroads, PEGASUS_BOOTS) # get to first chest via the north chest with pit buffering - east_bridge_chest.connect(first_bridge_chest, PEGASUS_BOOTS) # boots bonk across the pits with pit buffering + after_stalfos.connect(staircase_before_boss, r.zoomerang) # use zoomerang dashing left to get an unclipped boots superjump off the right wall over the block. reverse is push block + north_bridge_chest.connect(north_of_crossroads, r.boots_bonk_pit) # boots bonk across the pits with pit buffering + first_bridge_chest.connect(north_of_crossroads, r.boots_bonk_pit) # get to first chest via the north chest with pit buffering + east_bridge_chest.connect(first_bridge_chest, r.boots_bonk_pit) # boots bonk across the pits with pit buffering third_arena.connect(north_of_crossroads, SWORD) # can beat 3rd m.stalfos with 255 sword spins m_stalfos_drop.connect(third_arena, AND(FEATHER, SWORD)) # beat master stalfos by knocking it down 255 times x 4 (takes about 1.5h total) - m_stalfos_drop.connect(third_arena, AND(PEGASUS_BOOTS, SWORD)) # can reach fourth arena from entrance with pegasus boots and sword - boss_key.connect(after_stalfos, FLIPPERS) # pit buffer across + m_stalfos_drop.connect(third_arena, AND(r.boots_bonk_2d_hell, SWORD)) # can reach fourth arena from entrance with pegasus boots and sword + boss_key.connect(after_stalfos, AND(r.pit_buffer_itemless, FLIPPERS)) # pit buffer across if butterfly_owl: - after_keyblock_boss.connect(butterfly_owl, STONE_BEAK5, one_way=True) # pit buffer from top right to bottom in right pits room - before_boss.connect(after_stalfos, AND(FEATHER, SWORD)) # cross pits room from bottom left to top left by unclipped superjump on bottom wall on top of side wall, then jump across + after_keyblock_boss.connect(butterfly_owl, AND(r.pit_buffer_itemless, STONE_BEAK5), one_way=True) # pit buffer from top right to bottom in right pits room + before_boss.connect(after_stalfos, r.super_jump_sword) # cross pits room from bottom left to top left by unclipped superjump on bottom wall on top of side wall, then jump across self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon6.py b/worlds/ladx/LADXR/logic/dungeon6.py index d67138b334a6..cde40a6b2df4 100644 --- a/worlds/ladx/LADXR/logic/dungeon6.py +++ b/worlds/ladx/LADXR/logic/dungeon6.py @@ -6,8 +6,8 @@ class Dungeon6: def __init__(self, options, world_setup, r, *, raft_game_chest=True): entrance = Location(dungeon=6) - Location(dungeon=6).add(DungeonChest(0x1CF)).connect(entrance, OR(BOMB, BOW, MAGIC_ROD, COUNT(POWER_BRACELET, 2))) # 50 rupees - Location(dungeon=6).add(DungeonChest(0x1C9)).connect(entrance, COUNT(POWER_BRACELET, 2)) # 100 rupees start + Location(dungeon=6).add(DungeonChest(0x1CF)).connect(entrance, OR(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # 50 rupees + elephants_heart_chest = Location(dungeon=6).add(DungeonChest(0x1C9)).connect(entrance, COUNT(POWER_BRACELET, 2)) # 100 rupees start if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=6).add(OwlStatue(0x1BB)).connect(entrance, STONE_BEAK6) @@ -15,9 +15,9 @@ def __init__(self, options, world_setup, r, *, raft_game_chest=True): bracelet_chest = Location(dungeon=6).add(DungeonChest(0x1CE)).connect(entrance, AND(BOMB, FEATHER)) # left side - Location(dungeon=6).add(DungeonChest(0x1C0)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOW, MAGIC_ROD))) # 3 wizrobes raised blocks dont need to hit the switch + Location(dungeon=6).add(DungeonChest(0x1C0)).connect(entrance, AND(POWER_BRACELET, r.attack_wizrobe)) # 3 wizrobes raised blocks don't need to hit the switch left_side = Location(dungeon=6).add(DungeonChest(0x1B9)).add(DungeonChest(0x1B3)).connect(entrance, AND(POWER_BRACELET, OR(BOMB, BOOMERANG))) - Location(dungeon=6).add(DroppedKey(0x1B4)).connect(left_side, OR(BOMB, BOW, MAGIC_ROD)) # 2 wizrobe drop key + Location(dungeon=6).add(DroppedKey(0x1B4)).connect(left_side, OR(r.attack_wizrobe, BOW)) # 2 wizrobe drop key, allow bow as only 2 top_left = Location(dungeon=6).add(DungeonChest(0x1B0)).connect(left_side, COUNT(POWER_BRACELET, 2)) # top left chest horseheads if raft_game_chest: Location().add(Chest(0x06C)).connect(top_left, POWER_BRACELET) # seashell chest in raft game @@ -25,14 +25,15 @@ def __init__(self, options, world_setup, r, *, raft_game_chest=True): # right side to_miniboss = Location(dungeon=6).connect(entrance, KEY6) miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping[5]])) - lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(OR(BOMB, BOW, MAGIC_ROD), COUNT(POWER_BRACELET, 2))) # waterway key + lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # waterway key medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine if options.owlstatues == "both" or options.owlstatues == "dungeon": lower_right_owl = Location(dungeon=6).add(OwlStatue(0x1D7)).connect(lower_right_side, AND(POWER_BRACELET, STONE_BEAK6)) center_1 = Location(dungeon=6).add(DroppedKey(0x1C3)).connect(miniboss, AND(COUNT(POWER_BRACELET, 2), FEATHER)) # tile room key drop - center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, AND(KEY6, FOUND(KEY6, 2))) # top right chest horseheads + center_2_and_upper_right_side = Location(dungeon=6).add(DungeonChest(0x1B1)).connect(center_1, AND(COUNT(POWER_BRACELET, 2), PEGASUS_BOOTS, r.attack_pols_voice, KEY6, FOUND(KEY6, 2))) # top right chest horseheads boss_key = Location(dungeon=6).add(DungeonChest(0x1B6)).connect(center_2_and_upper_right_side, AND(AND(KEY6, FOUND(KEY6, 3), HOOKSHOT))) + center_2_and_upper_right_side.connect(boss_key, AND(HOOKSHOT, POWER_BRACELET, KEY6, FOUND(KEY6, 3)), one_way=True) if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=6).add(OwlStatue(0x1B6)).connect(boss_key, STONE_BEAK6) @@ -40,19 +41,22 @@ def __init__(self, options, world_setup, r, *, raft_game_chest=True): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': bracelet_chest.connect(entrance, BOMB) # get through 2d section by "fake" jumping to the ladders - center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2), PEGASUS_BOOTS)) # use a boots dash to get over the platforms - + center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2), r.boots_dash_2d)) # use a boots dash to get over the platforms + center_2_and_upper_right_side.connect(center_1, AND(COUNT(POWER_BRACELET, 2), r.damage_boost, r.attack_pols_voice, FOUND(KEY6, 2))) # damage_boost past the mini_thwomps + if options.logic == 'glitched' or options.logic == 'hell': - entrance.connect(left_side, AND(POWER_BRACELET, FEATHER), one_way=True) # path from entrance to left_side: use superjumps to pass raised blocks - lower_right_side.connect(center_2_and_upper_right_side, AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD)), one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block, so weapons added - center_2_and_upper_right_side.connect(center_1, AND(POWER_BRACELET, FEATHER), one_way=True) # going backwards from dodongos, use a shaq jump to pass by keyblock at tile room - boss_key.connect(lower_right_side, FEATHER) # superjump from waterway to the left. POWER_BRACELET is implied from lower_right_side + elephants_heart_chest.connect(entrance, BOMB) # kill moldorm on screen above wizrobes, then bomb trigger on the right side to break elephant statue to get to the second chest + entrance.connect(left_side, AND(POWER_BRACELET, r.super_jump_feather), one_way=True) # path from entrance to left_side: use superjumps to pass raised blocks + lower_right_side.connect(center_2_and_upper_right_side, r.super_jump, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block, so weapons added + center_1.connect(miniboss, AND(r.bomb_trigger, OR(r.boots_dash_2d, FEATHER))) # bomb trigger the elephant statue after the miniboss + center_2_and_upper_right_side.connect(center_1, AND(POWER_BRACELET, r.shaq_jump), one_way=True) # going backwards from dodongos, use a shaq jump to pass by keyblock at tile room + boss_key.connect(lower_right_side, AND(POWER_BRACELET, r.super_jump_feather)) # superjump from waterway to the left. if options.logic == 'hell': - entrance.connect(left_side, AND(POWER_BRACELET, PEGASUS_BOOTS, OR(BOW, MAGIC_ROD)), one_way=True) # can boots superhop off the top right corner in 3 wizrobe raised blocks room - medicine_chest.connect(lower_right_side, AND(PEGASUS_BOOTS, OR(MAGIC_ROD, BOW))) # can boots superhop off the top wall with bow or magic rod - center_1.connect(miniboss, AND(COUNT(POWER_BRACELET, 2))) # use a double damage boost from the sparks to get across (first one is free, second one needs to buffer while in midair for spark to get close enough) - lower_right_side.connect(center_2_and_upper_right_side, FEATHER, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block is super tight to get enough horizontal distance + entrance.connect(left_side, AND(POWER_BRACELET, r.boots_superhop), one_way=True) # can boots superhop off the top right corner in 3 wizrobe raised blocks room + medicine_chest.connect(lower_right_side, r.boots_superhop) # can boots superhop off the top wall with bow or magic rod + center_1.connect(miniboss, AND(r.damage_boost_special, OR(r.bomb_trigger, COUNT(POWER_BRACELET, 2)))) # use a double damage boost from the sparks to get across (first one is free, second one needs to buffer while in midair for spark to get close enough) + lower_right_side.connect(center_2_and_upper_right_side, r.super_jump_feather, one_way=True) # path from lower_right_side to center_2: superjump from waterway towards dodongos. superjump next to corner block is super tight to get enough horizontal distance self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon7.py b/worlds/ladx/LADXR/logic/dungeon7.py index 594b4d083ca7..6188138f38ef 100644 --- a/worlds/ladx/LADXR/logic/dungeon7.py +++ b/worlds/ladx/LADXR/logic/dungeon7.py @@ -14,8 +14,8 @@ def __init__(self, options, world_setup, r): if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=7).add(OwlStatue(0x204)).connect(topright_pillar_area, STONE_BEAK7) topright_pillar_area.add(DungeonChest(0x209)) # stone slab chest can be reached by dropping down a hole - three_of_a_kind_north = Location(dungeon=7).add(DungeonChest(0x211)).connect(topright_pillar_area, OR(r.attack_hookshot, AND(FEATHER, SHIELD))) # compass chest; path without feather with hitting switch by falling on the raised blocks. No bracelet because ball does not reset - bottomleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.attack_hookshot) # area with hinox, be able to hit a switch to reach that area + three_of_a_kind_north = Location(dungeon=7).add(DungeonChest(0x211)).connect(topright_pillar_area, OR(AND(r.hit_switch, r.attack_hookshot_no_bomb), AND(OR(BOMB, FEATHER), SHIELD))) # compass chest; either hit the switch, or have feather to fall on top of raised blocks. No bracelet because ball does not reset + bottomleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.hit_switch) # area with hinox, be able to hit a switch to reach that area topleftF1_chest = Location(dungeon=7).add(DungeonChest(0x201)) # top left chest on F1 bottomleftF2_area.connect(topleftF1_chest, None, one_way = True) # drop down in left most holes of hinox room or tile room Location(dungeon=7).add(DroppedKey(0x21B)).connect(bottomleftF2_area, r.attack_hookshot) # hinox drop key @@ -23,9 +23,9 @@ def __init__(self, options, world_setup, r): if options.owlstatues == "both" or options.owlstatues == "dungeon": bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7)) nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping[6]]) # nightmare key after the miniboss - mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.attack_hookshot) # mirror shield chest, need to be able to hit a switch to reach or + mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.hit_switch) # mirror shield chest, need to be able to hit a switch to reach or bottomleftF2_area.connect(mirror_shield_chest, AND(KEY7, FOUND(KEY7, 3)), one_way = True) # reach mirror shield chest from hinox area by opening keyblock - toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.attack_hookshot) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up + toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.hit_switch) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up final_pillar_area = Location(dungeon=7).add(DungeonChest(0x21C)).connect(bottomleftF2_area, AND(BOMB, HOOKSHOT)) # chest that needs to spawn to get to the last pillar final_pillar = Location(dungeon=7).connect(final_pillar_area, POWER_BRACELET) # decouple chest from pillar @@ -33,25 +33,28 @@ def __init__(self, options, world_setup, r): beamos_horseheads = Location(dungeon=7).add(DungeonChest(0x220)).connect(beamos_horseheads_area, POWER_BRACELET) # 100 rupee chest / medicine chest (DX) behind boss door pre_boss = Location(dungeon=7).connect(beamos_horseheads_area, HOOKSHOT) # raised plateau before boss staircase boss = Location(dungeon=7).add(HeartContainer(0x223), Instrument(0x22c)).connect(pre_boss, r.boss_requirements[world_setup.boss_mapping[6]]) - + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + three_of_a_kind_north.connect(topright_pillar_area, BOMB) # use timed bombs to match the 3 of a kinds (south 3 of a kind room is implicite as normal logic can not reach chest without hookshot) + if options.logic == 'glitched' or options.logic == 'hell': - topright_pillar_area.connect(entrance, AND(FEATHER, SWORD)) # superjump in the center to get on raised blocks, superjump in switch room to right side to walk down. center superjump has to be low so sword added - toprightF1_chest.connect(topright_pillar_area, FEATHER) # superjump from F1 switch room - topleftF2_area = Location(dungeon=7).connect(topright_pillar_area, FEATHER) # superjump in top left pillar room over the blocks from right to left, to reach tile room + topright_pillar_area.connect(entrance, r.super_jump_sword) # superjump in the center to get on raised blocks, superjump in switch room to right side to walk down. center superjump has to be low so sword added + toprightF1_chest.connect(topright_pillar_area, r.super_jump_feather) # superjump from F1 switch room + topleftF2_area = Location(dungeon=7).connect(topright_pillar_area, r.super_jump_feather) # superjump in top left pillar room over the blocks from right to left, to reach tile room topleftF2_area.connect(topleftF1_chest, None, one_way = True) # fall down tile room holes on left side to reach top left chest on ground floor - topleftF1_chest.connect(bottomleftF2_area, AND(PEGASUS_BOOTS, FEATHER), one_way = True) # without hitting the switch, jump on raised blocks at f1 pegs chest (0x209), and boots jump to stairs to reach hinox area - final_pillar_area.connect(bottomleftF2_area, OR(r.attack_hookshot, POWER_BRACELET, AND(FEATHER, SHIELD))) # sideways block push to get to the chest and pillar, kill requirement for 3 of a kind enemies to access chest. Assumes you do not get ball stuck on raised pegs for bracelet path + topleftF1_chest.connect(bottomleftF2_area, r.boots_jump, one_way = True) # without hitting the switch, jump on raised blocks at f1 pegs chest (0x209), and boots jump to stairs to reach hinox area + final_pillar_area.connect(bottomleftF2_area, AND(r.sideways_block_push, OR(r.attack_hookshot, POWER_BRACELET, AND(FEATHER, SHIELD)))) # sideways block push to get to the chest and pillar, kill requirement for 3 of a kind enemies to access chest. Assumes you do not get ball stuck on raised pegs for bracelet path if options.owlstatues == "both" or options.owlstatues == "dungeon": - bottomleft_owl.connect(bottomleftF2_area, STONE_BEAK7) # sideways block push to get to the owl statue + bottomleft_owl.connect(bottomleftF2_area, AND(r.sideways_block_push, STONE_BEAK7)) # sideways block push to get to the owl statue final_pillar.connect(bottomleftF2_area, BOMB) # bomb trigger pillar - pre_boss.connect(final_pillar, FEATHER) # superjump on top of goomba to extend superjump to boss door plateau + pre_boss.connect(final_pillar, r.super_jump_feather) # superjump on top of goomba to extend superjump to boss door plateau pre_boss.connect(beamos_horseheads_area, None, one_way=True) # can drop down from raised plateau to beamos horseheads area if options.logic == 'hell': - topright_pillar_area.connect(entrance, FEATHER) # superjump in the center to get on raised blocks, has to be low - topright_pillar_area.connect(entrance, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop in the center to get on raised blocks - toprightF1_chest.connect(topright_pillar_area, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop from F1 switch room - pre_boss.connect(final_pillar, AND(PEGASUS_BOOTS, OR(BOW, MAGIC_ROD))) # boots superhop on top of goomba to extend superhop to boss door plateau + topright_pillar_area.connect(entrance, r.super_jump_feather) # superjump in the center to get on raised blocks, has to be low + topright_pillar_area.connect(entrance, r.boots_superhop) # boots superhop in the center to get on raised blocks + toprightF1_chest.connect(topright_pillar_area, r.boots_superhop) # boots superhop from F1 switch room + pre_boss.connect(final_pillar, r.boots_superhop) # boots superhop on top of goomba to extend superhop to boss door plateau self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeon8.py b/worlds/ladx/LADXR/logic/dungeon8.py index 4444ecbb1419..5da2f8234ec4 100644 --- a/worlds/ladx/LADXR/logic/dungeon8.py +++ b/worlds/ladx/LADXR/logic/dungeon8.py @@ -11,7 +11,10 @@ def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): # left side entrance_left.add(DungeonChest(0x24D)) # zamboni room chest - Location(dungeon=8).add(DungeonChest(0x25C)).connect(entrance_left, r.attack_hookshot) # eye magnet chest + eye_magnet_chest = Location(dungeon=8).add(DungeonChest(0x25C)) # eye magnet chest bottom left below rolling bones + eye_magnet_chest.connect(entrance_left, OR(BOW, MAGIC_ROD, BOOMERANG, AND(FEATHER, r.attack_hookshot))) # damageless roller should be default + if options.hardmode != "ohko": + eye_magnet_chest.connect(entrance_left, r.attack_hookshot) # can take a hit vire_drop_key = Location(dungeon=8).add(DroppedKey(0x24C)).connect(entrance_left, r.attack_hookshot_no_bomb) # vire drop key sparks_chest = Location(dungeon=8).add(DungeonChest(0x255)).connect(entrance_left, OR(HOOKSHOT, FEATHER)) # chest before lvl1 miniboss Location(dungeon=8).add(DungeonChest(0x246)).connect(entrance_left, MAGIC_ROD) # key chest that spawns after creating fire @@ -30,7 +33,7 @@ def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): upper_center = Location(dungeon=8).connect(lower_center, AND(KEY8, FOUND(KEY8, 2))) if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=8).add(OwlStatue(0x245)).connect(upper_center, STONE_BEAK8) - Location(dungeon=8).add(DroppedKey(0x23E)).connect(upper_center, r.attack_skeleton) # 2 gibdos cracked floor; technically possible to use pits to kill but dumb + gibdos_drop_key = Location(dungeon=8).add(DroppedKey(0x23E)).connect(upper_center, r.attack_gibdos) # 2 gibdos cracked floor; technically possible to use pits to kill but dumb medicine_chest = Location(dungeon=8).add(DungeonChest(0x235)).connect(upper_center, AND(FEATHER, HOOKSHOT)) # medicine chest middle_center_1 = Location(dungeon=8).connect(upper_center, BOMB) @@ -66,33 +69,36 @@ def __init__(self, options, world_setup, r, *, back_entrance_heartpiece=True): if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': entrance_left.connect(entrance, BOMB) # use bombs to kill vire and hinox - vire_drop_key.connect(entrance_left, BOMB) # use bombs to kill rolling bones and vire - bottom_right.connect(slime_chest, FEATHER) # diagonal jump over the pits to reach rolling rock / zamboni + up_left.connect(vire_drop_key, BOMB, one_way=True) # use bombs to kill rolling bones and vire, do not allow pathway through hinox with just bombs, as not enough bombs are available + bottom_right.connect(slime_chest, r.tight_jump) # diagonal jump over the pits to reach rolling rock / zamboni + gibdos_drop_key.connect(upper_center, OR(HOOKSHOT, MAGIC_ROD)) # crack one of the floor tiles and hookshot the gibdos in, or burn the gibdos and make them jump into pit up_left.connect(lower_center, AND(BOMB, FEATHER)) # blow up hidden walls from peahat room -> dark room -> eye statue room slime_chest.connect(entrance, AND(r.attack_hookshot_powder, POWER_BRACELET)) # kill vire with powder or bombs if options.logic == 'glitched' or options.logic == 'hell': - sparks_chest.connect(entrance_left, OR(r.attack_hookshot, FEATHER, PEGASUS_BOOTS)) # 1 pit buffer across the pit. Add requirements for all the options to get to this area - lower_center.connect(entrance_up, None) # sideways block push in peahat room to get past keyblock - miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, HOOKSHOT)) # blow up hidden wall for darkroom, use feather + hookshot to clip past keyblock in front of stairs - miniboss_entrance.connect(lower_center, AND(BOMB, FEATHER, FOUND(KEY8, 7))) # same as above, but without clipping past the keyblock - up_left.connect(lower_center, FEATHER) # use jesus jump in refill room left of peahats to clip bottom wall and push bottom block left, to get a place to super jump - up_left.connect(upper_center, FEATHER) # from up left you can jesus jump / lava swim around the key door next to the boss. - top_left_stairs.connect(up_left, AND(FEATHER, SWORD)) # superjump - medicine_chest.connect(upper_center, FEATHER) # jesus super jump - up_left.connect(bossdoor, FEATHER, one_way=True) # superjump off the bottom or right wall to jump over to the boss door + sparks_chest.connect(entrance_left, r.pit_buffer_itemless) # 1 pit buffer across the pit. + entrance_up.connect(bottomright_pot_chest, r.super_jump_boots, one_way = True) # underground section with fire balls jumping up out of lava. Use boots superjump off left wall to jump over the pot blocking the way + lower_center.connect(entrance_up, r.sideways_block_push) # sideways block push in peahat room to get past keyblock + miniboss_entrance.connect(lower_center, AND(BOMB, r.bookshot)) # blow up hidden wall for darkroom, use feather + hookshot to clip past keyblock in front of stairs + miniboss_entrance.connect(lower_center, AND(BOMB, r.super_jump_feather, FOUND(KEY8, 7))) # same as above, but without clipping past the keyblock + up_left.connect(lower_center, r.jesus_jump) # use jesus jump in refill room left of peahats to clip bottom wall and push bottom block left, to get a place to super jump + up_left.connect(upper_center, r.jesus_jump) # from up left you can jesus jump / lava swim around the key door next to the boss. + top_left_stairs.connect(up_left, r.super_jump_feather) # superjump + medicine_chest.connect(upper_center, AND(r.super_jump_feather, r.jesus_jump)) # jesus super jump + up_left.connect(bossdoor, r.super_jump_feather, one_way=True) # superjump off the bottom or right wall to jump over to the boss door if options.logic == 'hell': if bottomright_owl: - bottomright_owl.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS, STONE_BEAK8)) # underground section past mimics, boots bonking across the gap to the ladder - bottomright_pot_chest.connect(entrance, AND(SWORD, POWER_BRACELET, PEGASUS_BOOTS)) # underground section past mimics, boots bonking across the gap to the ladder - entrance.connect(bottomright_pot_chest, AND(FEATHER, SWORD), one_way=True) # use NW zamboni staircase backwards, subpixel manip for superjump past the pots - medicine_chest.connect(upper_center, AND(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section - miniboss.connect(miniboss_entrance, AND(PEGASUS_BOOTS, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks - top_left_stairs.connect(map_chest, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section - nightmare_key.connect(top_left_stairs, AND(PEGASUS_BOOTS, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room - bottom_right.connect(entrance_up, AND(POWER_BRACELET, PEGASUS_BOOTS), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni - bossdoor.connect(entrance_up, AND(PEGASUS_BOOTS, MAGIC_ROD)) # boots bonk through 2d section + bottomright_owl.connect(entrance, AND(SWORD, POWER_BRACELET, r.boots_bonk_2d_hell, STONE_BEAK8)) # underground section past mimics, boots bonking across the gap to the ladder + bottomright_pot_chest.connect(entrance, AND(SWORD, POWER_BRACELET, r.boots_bonk_2d_hell)) # underground section past mimics, boots bonking across the gap to the ladder + entrance.connect(bottomright_pot_chest, r.shaq_jump, one_way=True) # use NW zamboni staircase backwards, and get a naked shaq jump off the bottom wall in the bottom right corner to pass by the pot + gibdos_drop_key.connect(upper_center, AND(FEATHER, SHIELD)) # lock gibdos into pits and crack the tile they stand on, then use shield to bump them into the pit + medicine_chest.connect(upper_center, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section + miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks + top_left_stairs.connect(map_chest, AND(r.jesus_buffer, r.boots_bonk_2d_hell, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section + nightmare_key.connect(top_left_stairs, AND(r.boots_bonk_pit, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room + bottom_right.connect(entrance_up, AND(POWER_BRACELET, r.jesus_buffer), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni + bossdoor.connect(entrance_up, AND(r.boots_bonk_2d_hell, MAGIC_ROD)) # boots bonk through 2d section self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/dungeonColor.py b/worlds/ladx/LADXR/logic/dungeonColor.py index aa58c0bafa91..fc14f70dd7a6 100644 --- a/worlds/ladx/LADXR/logic/dungeonColor.py +++ b/worlds/ladx/LADXR/logic/dungeonColor.py @@ -10,7 +10,7 @@ def __init__(self, options, world_setup, r): room2.add(DungeonChest(0x314)) # key if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=9).add(OwlStatue(0x308), OwlStatue(0x30F)).connect(room2, STONE_BEAK9) - room2_weapon = Location(dungeon=9).connect(room2, r.attack_hookshot) + room2_weapon = Location(dungeon=9).connect(room2, AND(r.attack_hookshot, POWER_BRACELET)) room2_weapon.add(DungeonChest(0x311)) # stone beak room2_lights = Location(dungeon=9).connect(room2, OR(r.attack_hookshot, SHIELD)) room2_lights.add(DungeonChest(0x30F)) # compass chest @@ -20,22 +20,24 @@ def __init__(self, options, world_setup, r): room3 = Location(dungeon=9).connect(room2, AND(KEY9, FOUND(KEY9, 2), r.miniboss_requirements[world_setup.miniboss_mapping["c1"]])) # After the miniboss room4 = Location(dungeon=9).connect(room3, POWER_BRACELET) # need to lift a pot to reveal button room4.add(DungeonChest(0x306)) # map - room4karakoro = Location(dungeon=9).add(DroppedKey(0x307)).connect(room4, r.attack_hookshot) # require item to knock Karakoro enemies into shell + room4karakoro = Location(dungeon=9).add(DroppedKey(0x307)).connect(room4, AND(r.attack_hookshot, POWER_BRACELET)) # require item to knock Karakoro enemies into shell if options.owlstatues == "both" or options.owlstatues == "dungeon": Location(dungeon=9).add(OwlStatue(0x30A)).connect(room4, STONE_BEAK9) room5 = Location(dungeon=9).connect(room4, OR(r.attack_hookshot, SHIELD)) # lights room room6 = Location(dungeon=9).connect(room5, AND(KEY9, FOUND(KEY9, 3))) # room with switch and nightmare door - pre_boss = Location(dungeon=9).connect(room6, OR(r.attack_hookshot, AND(PEGASUS_BOOTS, FEATHER))) # before the boss, require item to hit switch or jump past raised blocks + pre_boss = Location(dungeon=9).connect(room6, OR(r.hit_switch, AND(PEGASUS_BOOTS, FEATHER))) # before the boss, require item to hit switch or jump past raised blocks boss = Location(dungeon=9).connect(pre_boss, AND(NIGHTMARE_KEY9, r.boss_requirements[world_setup.boss_mapping[8]])) boss.add(TunicFairy(0), TunicFairy(1)) if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': - room2.connect(entrance, POWER_BRACELET) # throw pots at enemies - pre_boss.connect(room6, FEATHER) # before the boss, jump past raised blocks without boots + room2.connect(entrance, r.throw_pot) # throw pots at enemies + room2_weapon.connect(room2, r.attack_hookshot_no_bomb) # knock the karakoro into the pit without picking them up. + pre_boss.connect(room6, r.tight_jump) # before the boss, jump past raised blocks without boots if options.logic == 'hell': - room2_weapon.connect(room2, SHIELD) # shield bump karakoro into the holes - room4karakoro.connect(room4, SHIELD) # shield bump karakoro into the holes + room2_weapon.connect(room2, r.attack_hookshot) # also have a bomb as option to knock the karakoro into the pit without bracelet + room2_weapon.connect(room2, r.shield_bump) # shield bump karakoro into the holes + room4karakoro.connect(room4, r.shield_bump) # shield bump karakoro into the holes self.entrance = entrance diff --git a/worlds/ladx/LADXR/logic/overworld.py b/worlds/ladx/LADXR/logic/overworld.py index 551cf8353f4a..54da90f8931d 100644 --- a/worlds/ladx/LADXR/logic/overworld.py +++ b/worlds/ladx/LADXR/logic/overworld.py @@ -19,10 +19,13 @@ def __init__(self, options, world_setup, r): Location().add(DroppedKey(0x1E4)).connect(rooster_cave, AND(OCARINA, SONG3)) papahl_house = Location("Papahl House") - papahl_house.connect(Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)), TRADING_ITEM_YOSHI_DOLL) + mamasha_trade = Location().add(TradeSequenceItem(0x2A6, TRADING_ITEM_RIBBON)) + papahl_house.connect(mamasha_trade, TRADING_ITEM_YOSHI_DOLL) - trendy_shop = Location("Trendy Shop").add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)) - #trendy_shop.connect(Location()) + trendy_shop = Location("Trendy Shop") + trendy_shop.connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50)) + outside_trendy = Location() + outside_trendy.connect(mabe_village, r.bush) self._addEntrance("papahl_house_left", mabe_village, papahl_house, None) self._addEntrance("papahl_house_right", mabe_village, papahl_house, None) @@ -61,9 +64,9 @@ def __init__(self, options, world_setup, r): self._addEntrance("banana_seller", sword_beach, banana_seller, r.bush) boomerang_cave = Location("Boomerang Cave") if options.boomerang == 'trade': - Location().add(BoomerangGuy()).connect(boomerang_cave, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL)) + Location().add(BoomerangGuy()).connect(boomerang_cave, AND(r.shuffled_magnifier, OR(BOOMERANG, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, FEATHER, SHOVEL))) elif options.boomerang == 'gift': - Location().add(BoomerangGuy()).connect(boomerang_cave, None) + Location().add(BoomerangGuy()).connect(boomerang_cave, r.shuffled_magnifier) self._addEntrance("boomerang_cave", sword_beach, boomerang_cave, BOMB) self._addEntranceRequirementExit("boomerang_cave", None) # if exiting, you do not need bombs @@ -84,7 +87,7 @@ def __init__(self, options, world_setup, r): crazy_tracy_hut_inside = Location("Crazy Tracy's House") Location().add(KeyLocation("MEDICINE2")).connect(crazy_tracy_hut_inside, FOUND("RUPEES", 50)) self._addEntrance("crazy_tracy", crazy_tracy_hut, crazy_tracy_hut_inside, None) - start_house.connect(crazy_tracy_hut, SONG2, one_way=True) # Manbo's Mambo into the pond outside Tracy + start_house.connect(crazy_tracy_hut, AND(OCARINA, SONG2), one_way=True) # Manbo's Mambo into the pond outside Tracy forest_madbatter = Location("Forest Mad Batter") Location().add(MadBatter(0x1E1)).connect(forest_madbatter, MAGIC_POWDER) @@ -92,7 +95,7 @@ def __init__(self, options, world_setup, r): self._addEntranceRequirementExit("forest_madbatter", None) # if exiting, you do not need bracelet forest_cave = Location("Forest Cave") - Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom + forest_cave_crystal_chest = Location().add(Chest(0x2BD)).connect(forest_cave, SWORD) # chest in forest cave on route to mushroom log_cave_heartpiece = Location().add(HeartPiece(0x2AB)).connect(forest_cave, POWER_BRACELET) # piece of heart in the forest cave on route to the mushroom forest_toadstool = Location().add(Toadstool()) self._addEntrance("toadstool_entrance", forest, forest_cave, None) @@ -130,6 +133,7 @@ def __init__(self, options, world_setup, r): self._addEntranceRequirementExit("d0", None) # if exiting, you do not need bracelet ghost_grave = Location().connect(forest, POWER_BRACELET) Location().add(Seashell(0x074)).connect(ghost_grave, AND(r.bush, SHOVEL)) # next to grave cave, digging spot + graveyard.connect(forest_heartpiece, OR(BOOMERANG, HOOKSHOT), one_way=True) # grab the heart piece surrounded by pits from the north graveyard_cave_left = Location() graveyard_cave_right = Location().connect(graveyard_cave_left, OR(FEATHER, ROOSTER)) @@ -167,7 +171,9 @@ def __init__(self, options, world_setup, r): prairie_island_seashell = Location().add(Seashell(0x0A6)).connect(ukuku_prairie, AND(FLIPPERS, r.bush)) # next to lv3 Location().add(Seashell(0x08B)).connect(ukuku_prairie, r.bush) # next to seashell house Location().add(Seashell(0x0A4)).connect(ukuku_prairie, PEGASUS_BOOTS) # smash into tree next to phonehouse - self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER)) # left of the castle, 5 holes turned into 3 + self._addEntrance("castle_jump_cave", ukuku_prairie, Location().add(Chest(0x1FD)), ROOSTER) + if not options.rooster: + self._addEntranceRequirement("castle_jump_cave", AND(FEATHER, PEGASUS_BOOTS)) # left of the castle, 5 holes turned into 3 Location().add(Seashell(0x0B9)).connect(ukuku_prairie, POWER_BRACELET) # under the rock left_bay_area = Location() @@ -192,6 +198,7 @@ def __init__(self, options, world_setup, r): bay_madbatter_connector_exit = Location().connect(bay_madbatter_connector_entrance, FLIPPERS) bay_madbatter_connector_outside = Location() bay_madbatter = Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER) + outside_bay_madbatter_entrance = Location() self._addEntrance("prairie_madbatter_connector_entrance", left_bay_area, bay_madbatter_connector_entrance, AND(OR(FEATHER, ROOSTER), OR(SWORD, MAGIC_ROD, BOOMERANG))) self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), r.bush)) # if exiting, you can pick up the bushes by normal means self._addEntrance("prairie_madbatter_connector_exit", bay_madbatter_connector_outside, bay_madbatter_connector_exit, None) @@ -237,7 +244,8 @@ def __init__(self, options, world_setup, r): castle_courtyard = Location() castle_frontdoor = Location().connect(castle_courtyard, r.bush) castle_frontdoor.connect(ukuku_prairie, "CASTLE_BUTTON") # the button in the castle connector allows access to the castle grounds in ER - self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("castle_secret_entrance", next_to_castle, castle_secret_entrance_right, r.pit_bush) + self._addEntranceRequirementExit("castle_secret_entrance", None) # leaving doesn't require pit_bush self._addEntrance("castle_secret_exit", castle_courtyard, castle_secret_entrance_left, None) Location().add(HeartPiece(0x078)).connect(bay_water, FLIPPERS) # in the moat of the castle @@ -245,7 +253,7 @@ def __init__(self, options, world_setup, r): Location().add(KeyLocation("CASTLE_BUTTON")).connect(castle_inside, None) castle_top_outside = Location() castle_top_inside = Location() - self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, r.bush) + self._addEntrance("castle_main_entrance", castle_frontdoor, castle_inside, None) self._addEntrance("castle_upper_left", castle_top_outside, castle_inside, None) self._addEntrance("castle_upper_right", castle_top_outside, castle_top_inside, None) Location().add(GoldLeaf(0x05A)).connect(castle_courtyard, OR(SWORD, BOW, MAGIC_ROD)) # mad bomber, enemy hiding in the 6 holes @@ -274,7 +282,8 @@ def __init__(self, options, world_setup, r): animal_village.connect(ukuku_prairie, OR(HOOKSHOT, ROOSTER)) animal_village_connector_left = Location() animal_village_connector_right = Location().connect(animal_village_connector_left, PEGASUS_BOOTS) - self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) # passage under river blocked by bush + self._addEntrance("prairie_to_animal_connector", ukuku_prairie, animal_village_connector_left, r.pit_bush) # passage under river blocked by bush + self._addEntranceRequirementExit("prairie_to_animal_connector", None) # leaving doesn't require pit_bush self._addEntrance("animal_to_prairie_connector", animal_village, animal_village_connector_right, None) if options.owlstatues == "both" or options.owlstatues == "overworld": animal_village.add(OwlStatue(0x0DA)) @@ -282,7 +291,7 @@ def __init__(self, options, world_setup, r): desert = Location().connect(animal_village, r.bush) # Note: We moved the walrus blocking the desert. if options.owlstatues == "both" or options.owlstatues == "overworld": desert.add(OwlStatue(0x0CF)) - desert_lanmola = Location().add(AnglerKey()).connect(desert, OR(BOW, SWORD, HOOKSHOT, MAGIC_ROD, BOOMERANG)) + desert_lanmola = Location().add(AnglerKey()).connect(desert, r.attack_hookshot_no_bomb) animal_village_bombcave = Location() self._addEntrance("animal_cave", desert, animal_village_bombcave, BOMB) @@ -296,13 +305,15 @@ def __init__(self, options, world_setup, r): Location().add(HeartPiece(0x1E8)).connect(desert_cave, BOMB) # above the quicksand cave Location().add(Seashell(0x0FF)).connect(desert, POWER_BRACELET) # bottom right corner of the map - armos_maze = Location().connect(animal_village, POWER_BRACELET) - armos_temple = Location() + armos_maze = Location("Armos Maze").connect(animal_village, POWER_BRACELET) + armos_temple = Location("Southern Shrine") Location().add(FaceKey()).connect(armos_temple, r.miniboss_requirements[world_setup.miniboss_mapping["armos_temple"]]) if options.owlstatues == "both" or options.owlstatues == "overworld": armos_maze.add(OwlStatue(0x08F)) - self._addEntrance("armos_maze_cave", armos_maze, Location().add(Chest(0x2FC)), None) - self._addEntrance("armos_temple", armos_maze, armos_temple, None) + outside_armos_cave = Location("Outside Armos Maze Cave").connect(armos_maze, OR(r.attack_hookshot, SHIELD)) + outside_armos_temple = Location("Outside Southern Shrine").connect(armos_maze, OR(r.attack_hookshot, SHIELD)) + self._addEntrance("armos_maze_cave", outside_armos_cave, Location().add(Chest(0x2FC)), None) + self._addEntrance("armos_temple", outside_armos_temple, armos_temple, None) armos_fairy_entrance = Location().connect(bay_water, FLIPPERS).connect(animal_village, POWER_BRACELET) self._addEntrance("armos_fairy", armos_fairy_entrance, None, BOMB) @@ -347,17 +358,21 @@ def __init__(self, options, world_setup, r): lower_right_taltal.connect(below_right_taltal, FLIPPERS, one_way=True) heartpiece_swim_cave = Location().connect(Location().add(HeartPiece(0x1F2)), FLIPPERS) + outside_swim_cave = Location() + below_right_taltal.connect(outside_swim_cave, FLIPPERS) self._addEntrance("heartpiece_swim_cave", below_right_taltal, heartpiece_swim_cave, FLIPPERS) # cave next to level 4 d4_entrance = Location().connect(below_right_taltal, FLIPPERS) lower_right_taltal.connect(d4_entrance, AND(ANGLER_KEY, "ANGLER_KEYHOLE"), one_way=True) self._addEntrance("d4", d4_entrance, None, ANGLER_KEY) self._addEntranceRequirementExit("d4", FLIPPERS) # if exiting, you can leave with flippers without opening the dungeon + outside_mambo = Location("Outside Manbo").connect(d4_entrance, FLIPPERS) + inside_mambo = Location("Manbo's Cave") mambo = Location().connect(Location().add(Song(0x2FD)), AND(OCARINA, FLIPPERS)) # Manbo's Mambo - self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) + self._addEntrance("mambo", d4_entrance, mambo, FLIPPERS) # Raft game. raft_house = Location("Raft House") - Location().add(KeyLocation("RAFT")).connect(raft_house, COUNT("RUPEES", 100)) + Location().add(KeyLocation("RAFT")).connect(raft_house, AND(r.bush, COUNT("RUPEES", 100))) # add bush requirement for farming in case player has to try again raft_return_upper = Location() raft_return_lower = Location().connect(raft_return_upper, None, one_way=True) outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True) @@ -379,7 +394,9 @@ def __init__(self, options, world_setup, r): self._addEntrance("rooster_house", outside_rooster_house, None, None) bird_cave = Location() bird_key = Location().add(BirdKey()) - bird_cave.connect(bird_key, OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER)) + bird_cave.connect(bird_key, ROOSTER) + if not options.rooster: + bird_cave.connect(bird_key, AND(FEATHER, COUNT(POWER_BRACELET, 2))) # elephant statue added if options.logic != "casual": bird_cave.connect(lower_right_taltal, None, one_way=True) # Drop in a hole at bird cave self._addEntrance("bird_cave", outside_rooster_house, bird_cave, None) @@ -387,10 +404,13 @@ def __init__(self, options, world_setup, r): multichest_cave = Location() multichest_cave_secret = Location().connect(multichest_cave, BOMB) + multichest_cave.connect(multichest_cave_secret, BOMB, one_way=True) water_cave_hole = Location() # Location with the hole that drops you onto the hearth piece under water if options.logic != "casual": water_cave_hole.connect(heartpiece_swim_cave, FLIPPERS, one_way=True) + outside_multichest_left = Location() multichest_outside = Location().add(Chest(0x01D)) # chest after multichest puzzle outside + lower_right_taltal.connect(outside_multichest_left, OR(FLIPPERS, ROOSTER)) self._addEntrance("multichest_left", lower_right_taltal, multichest_cave, OR(FLIPPERS, ROOSTER)) self._addEntrance("multichest_right", water_cave_hole, multichest_cave, None) self._addEntrance("multichest_top", multichest_outside, multichest_cave_secret, None) @@ -428,7 +448,7 @@ def __init__(self, options, world_setup, r): left_right_connector_cave_exit = Location() left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, OR(HOOKSHOT, ROOSTER), one_way=True) # pass through the underground passage to left side taltal_boulder_zone = Location() - self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD)) + self._addEntrance("left_to_right_taltalentrance", mountain_bridge_staircase, left_right_connector_cave_entrance, r.pit_bush) self._addEntrance("left_taltal_entrance", taltal_boulder_zone, left_right_connector_cave_exit, None) mountain_heartpiece = Location().add(HeartPiece(0x2BA)) # heartpiece in connecting cave left_right_connector_cave_entrance.connect(mountain_heartpiece, BOMB, one_way=True) # in the connecting cave from right to left. one_way to prevent access to left_side_mountain via glitched logic @@ -460,130 +480,169 @@ def __init__(self, options, world_setup, r): windfish = Location("Windfish").connect(nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW))) if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': - hookshot_cave.connect(hookshot_cave_chest, AND(FEATHER, PEGASUS_BOOTS)) # boots jump the gap to the chest - graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT, one_way=True) # hookshot the block behind the stairs while over the pit - swamp_chest.connect(swamp, None) # Clip past the flower + hookshot_cave.connect(hookshot_cave_chest, r.boots_jump) # boots jump the gap to the chest + graveyard_cave_left.connect(graveyard_cave_right, r.hookshot_over_pit, one_way=True) # hookshot the block behind the stairs while over the pit + swamp_chest.connect(swamp, r.wall_clip) # Clip past the flower self._addEntranceRequirement("d2", POWER_BRACELET) # clip the top wall to walk between the goponga flower and the wall self._addEntranceRequirement("d2", COUNT(SWORD, 2)) # use l2 sword spin to kill goponga flowers - swamp.connect(writes_hut_outside, HOOKSHOT, one_way=True) # hookshot the sign in front of writes hut + self._addEntranceRequirementExit("d2", r.wall_clip) # Clip out at d2 entrance door + swamp.connect(writes_hut_outside, r.hookshot_over_pit, one_way=True) # hookshot the sign in front of writes hut graveyard_heartpiece.connect(graveyard_cave_right, FEATHER) # jump to the bottom right tile around the blocks - graveyard_heartpiece.connect(graveyard_cave_right, OR(HOOKSHOT, BOOMERANG)) # push bottom block, wall clip and hookshot/boomerang corner to grab item - - self._addEntranceRequirement("mamu", AND(FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped + graveyard_heartpiece.connect(graveyard_cave_right, AND(r.wall_clip, OR(HOOKSHOT, BOOMERANG))) # push bottom block, wall clip and hookshot/boomerang corner to grab item + + self._addEntranceRequirement("mamu", AND(r.wall_clip, FEATHER, POWER_BRACELET)) # can clear the gaps at the start with just feather, can reach bottom left sign with a well timed jump while wall clipped self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(OR(FEATHER, ROOSTER), OR(MAGIC_POWDER, BOMB))) # use bombs or powder to get rid of a bush on the other side by jumping across and placing the bomb/powder before you fall into the pit - fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # can talk to the fisherman from the water when the boat is low (requires swimming up out of the water a bit) crow_gold_leaf.connect(castle_courtyard, POWER_BRACELET) # bird on tree at left side kanalet, can use both rocks to kill the crow removing the kill requirement castle_inside.connect(kanalet_chain_trooper, BOOMERANG, one_way=True) # kill the ball and chain trooper from the left side, then use boomerang to grab the dropped item - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(PEGASUS_BOOTS, FEATHER)) # jump across horizontal 4 gap to heart piece + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, r.boots_jump) # jump across horizontal 4 gap to heart piece + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(BOMB, FEATHER, BOOMERANG)) # use jump + boomerang to grab the item from below the ledge desert_lanmola.connect(desert, BOMB) # use bombs to kill lanmola - + + armos_maze.connect(outside_armos_cave, None) # dodge the armos statues by activating them and running + armos_maze.connect(outside_armos_temple, None) # dodge the armos statues by activating them and running d6_connector_left.connect(d6_connector_right, AND(OR(FLIPPERS, PEGASUS_BOOTS), FEATHER)) # jump the gap in underground passage to d6 left side to skip hookshot - bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue - fire_cave_bottom.connect(fire_cave_top, PEGASUS_BOOTS, one_way=True) # flame skip + obstacle_cave_exit.connect(obstacle_cave_inside, AND(FEATHER, r.hookshot_over_pit), one_way=True) # one way from right exit to middle, jump past the obstacle, and use hookshot to pull past the double obstacle + if not options.rooster: + bird_key.connect(bird_cave, COUNT(POWER_BRACELET, 2)) # corner walk past the one pit on the left side to get to the elephant statue + right_taltal_connector2.connect(right_taltal_connector3, ROOSTER, one_way=True) # jump off the ledge and grab rooster after landing on the pit + fire_cave_bottom.connect(fire_cave_top, AND(r.damage_boost_special, PEGASUS_BOOTS), one_way=True) # flame skip if options.logic == 'glitched' or options.logic == 'hell': + papahl_house.connect(mamasha_trade, r.bomb_trigger) # use a bomb trigger to trade with mamasha without having yoshi doll #self._addEntranceRequirement("dream_hut", FEATHER) # text clip TODO: require nag messages - self._addEntranceRequirementEnter("dream_hut", HOOKSHOT) # clip past the rocks in front of dream hut - dream_hut_right.connect(dream_hut_left, FEATHER) # super jump - forest.connect(swamp, BOMB) # bomb trigger tarin + self._addEntranceRequirementEnter("dream_hut", r.hookshot_clip) # clip past the rocks in front of dream hut + dream_hut_right.connect(dream_hut_left, r.super_jump_feather) # super jump + forest.connect(swamp, r.bomb_trigger) # bomb trigger tarin forest.connect(forest_heartpiece, BOMB, one_way=True) # bomb trigger heartpiece - self._addEntranceRequirementEnter("hookshot_cave", HOOKSHOT) # clip past the rocks in front of hookshot cave - swamp.connect(forest_toadstool, None, one_way=True) # villa buffer from top (swamp phonebooth area) to bottom (toadstool area) - writes_hut_outside.connect(swamp, None, one_way=True) # villa buffer from top (writes hut) to bottom (swamp phonebooth area) or damage boost + self._addEntranceRequirementEnter("hookshot_cave", r.hookshot_clip) # clip past the rocks in front of hookshot cave + swamp.connect(forest_toadstool, r.pit_buffer_itemless, one_way=True) # villa buffer from top (swamp phonebooth area) to bottom (toadstool area) + writes_hut_outside.connect(swamp, r.pit_buffer_itemless, one_way=True) # villa buffer from top (writes hut) to bottom (swamp phonebooth area) or damage boost graveyard.connect(forest_heartpiece, None, one_way=True) # villa buffer from top. - log_cave_heartpiece.connect(forest_cave, FEATHER) # super jump - log_cave_heartpiece.connect(forest_cave, BOMB) # bomb trigger - graveyard_cave_left.connect(graveyard_heartpiece, BOMB, one_way=True) # bomb trigger the heartpiece from the left side - graveyard_heartpiece.connect(graveyard_cave_right, None) # sideways block push from the right staircase. - - prairie_island_seashell.connect(ukuku_prairie, AND(FEATHER, r.bush)) # jesus jump from right side, screen transition on top of the water to reach the island - self._addEntranceRequirement("castle_jump_cave", FEATHER) # 1 pit buffer to clip bottom wall and jump across. - left_bay_area.connect(ghost_hut_outside, FEATHER) # 1 pit buffer to get across - tiny_island.connect(left_bay_area, AND(FEATHER, r.bush)) # jesus jump around - bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, FEATHER, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter - self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(FEATHER, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up - - ukuku_prairie.connect(richard_maze, OR(BOMB, BOOMERANG, MAGIC_POWDER, MAGIC_ROD, SWORD), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze - fisher_under_bridge.connect(bay_water, AND(BOMB, FLIPPERS)) # can bomb trigger the item without having the hook - animal_village.connect(ukuku_prairie, FEATHER) # jesus jump - below_right_taltal.connect(next_to_castle, FEATHER) # jesus jump (north of kanalet castle phonebooth) - animal_village_connector_right.connect(animal_village_connector_left, FEATHER) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text or shaq jump in bottom right corner if text is off - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(BOMB, OR(HOOKSHOT, FEATHER, PEGASUS_BOOTS))) # bomb trigger from right side, corner walking top right pit is stupid so hookshot or boots added - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, FEATHER) # villa buffer across the pits - - d6_entrance.connect(ukuku_prairie, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance bottom ledge to ukuku prairie - d6_entrance.connect(armos_fairy_entrance, FEATHER, one_way=True) # jesus jump (2 screen) from d6 entrance top ledge to armos fairy entrance - armos_fairy_entrance.connect(d6_armos_island, FEATHER, one_way=True) # jesus jump from top (fairy bomb cave) to armos island - armos_fairy_entrance.connect(raft_exit, FEATHER) # jesus jump (2-ish screen) from fairy cave to lower raft connector - self._addEntranceRequirementEnter("obstacle_cave_entrance", HOOKSHOT) # clip past the rocks in front of obstacle cave entrance - obstacle_cave_inside_chest.connect(obstacle_cave_inside, FEATHER) # jump to the rightmost pits + 1 pit buffer to jump across - obstacle_cave_exit.connect(obstacle_cave_inside, FEATHER) # 1 pit buffer above boots crystals to get past - lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, BOMB), one_way=True) # bomb trigger papahl from below ledge, requires pineapple - - self._addEntranceRequirement("heartpiece_swim_cave", FEATHER) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below - self._addEntranceRequirement("mambo", FEATHER) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance - outside_raft_house.connect(below_right_taltal, FEATHER, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south - - self._addEntranceRequirement("multichest_left", FEATHER) # jesus jump past staircase leading up the mountain - outside_rooster_house.connect(lower_right_taltal, FEATHER) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain + graveyard.connect(forest, None, one_way=True) # villa buffer from the top twice to get to the main forest area + log_cave_heartpiece.connect(forest_cave, r.super_jump_feather) # super jump + log_cave_heartpiece.connect(forest_cave, r.bomb_trigger) # bomb trigger + graveyard_cave_left.connect(graveyard_heartpiece, r.bomb_trigger, one_way=True) # bomb trigger the heartpiece from the left side + graveyard_heartpiece.connect(graveyard_cave_right, r.sideways_block_push) # sideways block push from the right staircase. + + prairie_island_seashell.connect(ukuku_prairie, AND(r.jesus_jump, r.bush)) # jesus jump from right side, screen transition on top of the water to reach the island + self._addEntranceRequirement("castle_jump_cave", r.pit_buffer) # 1 pit buffer to clip bottom wall and jump across. + left_bay_area.connect(ghost_hut_outside, r.pit_buffer) # 1 pit buffer to get across + tiny_island.connect(left_bay_area, AND(r.jesus_jump, r.bush)) # jesus jump around + bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, r.jesus_jump, one_way=True) # jesus jump (3 screen) through the underground passage leading to martha's bay mad batter + left_bay_area.connect(outside_bay_madbatter_entrance, AND(r.pit_buffer, POWER_BRACELET)) # villa buffer into the top side of the bush, then pick it up + + ukuku_prairie.connect(richard_maze, AND(r.pit_buffer_itemless, OR(AND(MAGIC_POWDER, MAX_POWDER_UPGRADE), BOMB, BOOMERANG, MAGIC_ROD, SWORD)), one_way=True) # break bushes on north side of the maze, and 1 pit buffer into the maze + richard_maze.connect(ukuku_prairie, AND(r.pit_buffer_itemless, OR(MAGIC_POWDER, BOMB, BOOMERANG, MAGIC_ROD, SWORD)), one_way=True) # same as above (without powder upgrade) in one of the two northern screens of the maze to escape + fisher_under_bridge.connect(bay_water, AND(r.bomb_trigger, AND(FEATHER, FLIPPERS))) # up-most left wall is a pit: bomb trigger with it. If photographer is there, clear that first which is why feather is required logically + animal_village.connect(ukuku_prairie, r.jesus_jump) # jesus jump + below_right_taltal.connect(next_to_castle, r.jesus_jump) # jesus jump (north of kanalet castle phonebooth) + #animal_village_connector_right.connect(animal_village_connector_left, AND(r.text_clip, FEATHER)) # text clip past the obstacles (can go both ways), feather to wall clip the obstacle without triggering text + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, AND(r.bomb_trigger, OR(HOOKSHOT, FEATHER, r.boots_bonk_pit))) # bomb trigger from right side, corner walking top right pit is stupid so hookshot or boots added + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, r.pit_buffer) # villa buffer across the pits + + d6_entrance.connect(ukuku_prairie, r.jesus_jump, one_way=True) # jesus jump (2 screen) from d6 entrance bottom ledge to ukuku prairie + d6_entrance.connect(armos_fairy_entrance, r.jesus_jump, one_way=True) # jesus jump (2 screen) from d6 entrance top ledge to armos fairy entrance + d6_connector_left.connect(d6_connector_right, r.jesus_jump) # jesus jump over water; left side is jumpable, or villa buffer if it's easier for you + armos_fairy_entrance.connect(d6_armos_island, r.jesus_jump, one_way=True) # jesus jump from top (fairy bomb cave) to armos island + armos_fairy_entrance.connect(raft_exit, r.jesus_jump) # jesus jump (2-ish screen) from fairy cave to lower raft connector + self._addEntranceRequirementEnter("obstacle_cave_entrance", r.hookshot_clip) # clip past the rocks in front of obstacle cave entrance + obstacle_cave_inside_chest.connect(obstacle_cave_inside, r.pit_buffer) # jump to the rightmost pits + 1 pit buffer to jump across + obstacle_cave_exit.connect(obstacle_cave_inside, r.pit_buffer) # 1 pit buffer above boots crystals to get past + lower_right_taltal.connect(hibiscus_item, AND(TRADING_ITEM_PINEAPPLE, r.bomb_trigger), one_way=True) # bomb trigger papahl from below ledge, requires pineapple + + self._addEntranceRequirement("heartpiece_swim_cave", r.jesus_jump) # jesus jump into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below + self._addEntranceRequirement("mambo", r.jesus_jump) # jesus jump from (unlocked) d4 entrance to mambo's cave entrance + outside_raft_house.connect(below_right_taltal, r.jesus_jump, one_way=True) # jesus jump from the ledge at raft to the staircase 1 screen south + + self._addEntranceRequirement("multichest_left", r.jesus_jump) # jesus jump past staircase leading up the mountain + outside_rooster_house.connect(lower_right_taltal, r.jesus_jump) # jesus jump (1 or 2 screen depending if angler key is used) to staircase leading up the mountain d7_platau.connect(water_cave_hole, None, one_way=True) # use save and quit menu to gain control while falling to dodge the water cave hole - mountain_bridge_staircase.connect(outside_rooster_house, AND(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across - bird_key.connect(bird_cave, AND(FEATHER, HOOKSHOT)) # hookshot jump across the big pits room - right_taltal_connector2.connect(right_taltal_connector3, None, one_way=True) # 2 seperate pit buffers so not obnoxious to get past the two pit rooms before d7 area. 2nd pits can pit buffer on top right screen, bottom wall to scroll on top of the wall on bottom screen - left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(HOOKSHOT, FEATHER), one_way=True) # pass through the passage in reverse using a superjump to get out of the dead end - obstacle_cave_inside.connect(mountain_heartpiece, BOMB, one_way=True) # bomb trigger from boots crystal cave - self._addEntranceRequirement("d8", OR(BOMB, AND(OCARINA, SONG3))) # bomb trigger the head and walk trough, or play the ocarina song 3 and walk through + mountain_bridge_staircase.connect(outside_rooster_house, AND(r.boots_jump, r.pit_buffer)) # cross bridge to staircase with pit buffer to clip bottom wall and jump across. added boots_jump to not require going through this section with just feather + bird_key.connect(bird_cave, r.hookshot_jump) # hookshot jump across the big pits room + right_taltal_connector2.connect(right_taltal_connector3, OR(r.pit_buffer, ROOSTER), one_way=True) # trigger a quick fall on the screen above the exit by transitioning down on the leftmost/rightmost pit and then buffering sq menu for control while in the air. or pick up the rooster while dropping off the ledge at exit + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(HOOKSHOT, r.super_jump_feather), one_way=True) # pass through the passage in reverse using a superjump to get out of the dead end + obstacle_cave_inside.connect(mountain_heartpiece, r.bomb_trigger, one_way=True) # bomb trigger from boots crystal cave + self._addEntranceRequirement("d8", OR(r.bomb_trigger, AND(OCARINA, SONG3))) # bomb trigger the head and walk through, or play the ocarina song 3 and walk through if options.logic == 'hell': dream_hut_right.connect(dream_hut, None) # alternate diagonal movement with orthogonal movement to control the mimics. Get them clipped into the walls to walk past - swamp.connect(forest_toadstool, None) # damage boost from toadstool area across the pit - swamp.connect(forest, AND(r.bush, OR(PEGASUS_BOOTS, HOOKSHOT))) # boots bonk / hookshot spam over the pits right of forest_rear_chest + swamp.connect(forest_toadstool, r.damage_boost) # damage boost from toadstool area across the pit + swamp.connect(forest, AND(r.bush, OR(r.boots_bonk_pit, r.hookshot_spam_pit))) # boots bonk / hookshot spam over the pits right of forest_rear_chest forest.connect(forest_heartpiece, PEGASUS_BOOTS, one_way=True) # boots bonk across the pits + forest_cave_crystal_chest.connect(forest_cave, AND(r.super_jump_feather, r.hookshot_clip_block, r.sideways_block_push)) # superjump off the bottom wall to get between block and crystal, than use 3 keese to hookshot clip while facing right to get a sideways blockpush off log_cave_heartpiece.connect(forest_cave, BOOMERANG) # clip the boomerang through the corner gaps on top right to grab the item - log_cave_heartpiece.connect(forest_cave, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD))) # boots rooster hop in bottom left corner to "superjump" into the area. use buffers after picking up rooster to gain height / time to throw rooster again facing up - writes_hut_outside.connect(swamp, None) # damage boost with moblin arrow next to telephone booth - writes_cave_left_chest.connect(writes_cave, None) # damage boost off the zol to get across the pit. - graveyard.connect(crazy_tracy_hut, HOOKSHOT, one_way=True) # use hookshot spam to clip the rock on the right with the crow - graveyard.connect(forest, OR(PEGASUS_BOOTS, HOOKSHOT)) # boots bonk witches hut, or hookshot spam across the pit - graveyard_cave_left.connect(graveyard_cave_right, HOOKSHOT) # hookshot spam over the pit - graveyard_cave_right.connect(graveyard_cave_left, PEGASUS_BOOTS, one_way=True) # boots bonk off the cracked block - - self._addEntranceRequirementEnter("mamu", AND(PEGASUS_BOOTS, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall - self._addEntranceRequirement("castle_jump_cave", PEGASUS_BOOTS) # pit buffer to clip bottom wall and boots bonk across - prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(PEGASUS_BOOTS, HOOKSHOT))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across - richard_cave_chest.connect(richard_cave, None) # use the zol on the other side of the pit to damage boost across (requires damage from pit + zol) - castle_secret_entrance_right.connect(castle_secret_entrance_left, AND(PEGASUS_BOOTS, "MEDICINE2")) # medicine iframe abuse to get across spikes with a boots bonk - left_bay_area.connect(ghost_hut_outside, PEGASUS_BOOTS) # multiple pit buffers to bonk across the bottom wall - tiny_island.connect(left_bay_area, AND(PEGASUS_BOOTS, r.bush)) # jesus jump around with boots bonks, then one final bonk off the bottom wall to get on the staircase (needs to be centered correctly) - self._addEntranceRequirement("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, OR(MAGIC_POWDER, BOMB, SWORD, MAGIC_ROD, BOOMERANG))) # Boots bonk across the bottom wall, then remove one of the bushes to get on land - self._addEntranceRequirementExit("prairie_madbatter_connector_entrance", AND(PEGASUS_BOOTS, r.bush)) # if exiting, you can pick up the bushes by normal means and boots bonk across the bottom wall + log_cave_heartpiece.connect(forest_cave, OR(r.super_jump_rooster, r.boots_roosterhop)) # boots rooster hop in bottom left corner to "superjump" into the area. use buffers after picking up rooster to gain height / time to throw rooster again facing up + writes_hut_outside.connect(swamp, r.damage_boost) # damage boost with moblin arrow next to telephone booth + writes_cave_left_chest.connect(writes_cave, r.damage_boost) # damage boost off the zol to get across the pit. + graveyard.connect(crazy_tracy_hut, r.hookshot_spam_pit, one_way=True) # use hookshot spam to clip the rock on the right with the crow + graveyard.connect(forest, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk over pits by witches hut, or hookshot spam across the pit + graveyard_cave_left.connect(graveyard_cave_right, r.hookshot_spam_pit) # hookshot spam over the pit + graveyard_cave_right.connect(graveyard_cave_left, OR(r.damage_boost, r.boots_bonk_pit), one_way=True) # boots bonk off the cracked block, or set up a damage boost with the keese + + self._addEntranceRequirementEnter("mamu", AND(r.pit_buffer_itemless, r.pit_buffer_boots, POWER_BRACELET)) # can clear the gaps at the start with multiple pit buffers, can reach bottom left sign with bonking along the bottom wall + self._addEntranceRequirement("castle_jump_cave", r.pit_buffer_boots) # pit buffer to clip bottom wall and boots bonk across + prairie_cave_secret_exit.connect(prairie_cave, AND(BOMB, OR(r.boots_bonk_pit, r.hookshot_spam_pit))) # hookshot spam or boots bonk across pits can go from left to right by pit buffering on top of the bottom wall then boots bonk across + richard_cave_chest.connect(richard_cave, r.damage_boost) # use the zol on the other side of the pit to damage boost across (requires damage from pit + zol) + castle_secret_entrance_right.connect(castle_secret_entrance_left, r.boots_bonk_2d_spikepit) # medicine iframe abuse to get across spikes with a boots bonk + left_bay_area.connect(ghost_hut_outside, r.pit_buffer_boots) # multiple pit buffers to bonk across the bottom wall + left_bay_area.connect(ukuku_prairie, r.hookshot_clip_block, one_way=True) # clip through the donuts blocking the path next to prairie plateau cave by hookshotting up and killing the two moblins that way which clips you further up two times. This is enough to move right + tiny_island.connect(left_bay_area, AND(r.jesus_buffer, r.boots_bonk_pit, r.bush)) # jesus jump around with boots bonks, then one final bonk off the bottom wall to get on the staircase (needs to be centered correctly) + left_bay_area.connect(outside_bay_madbatter_entrance, AND(r.pit_buffer_boots, OR(MAGIC_POWDER, SWORD, MAGIC_ROD, BOOMERANG))) # Boots bonk across the bottom wall, then remove one of the bushes to get on land + left_bay_area.connect(outside_bay_madbatter_entrance, AND(r.pit_buffer, r.hookshot_spam_pit, r.bush)) # hookshot spam to cross one pit at the top, then buffer until on top of the bush to be able to break it + outside_bay_madbatter_entrance.connect(left_bay_area, AND(r.pit_buffer_boots, r.bush), one_way=True) # if exiting, you can pick up the bushes by normal means and boots bonk across the bottom wall # bay_water connectors, only left_bay_area, ukuku_prairie and animal_village have to be connected with jesus jumps. below_right_taltal, d6_armos_island and armos_fairy_entrance are accounted for via ukuku prairie in glitch logic - left_bay_area.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) - animal_village.connect(bay_water, FEATHER) # jesus jump (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) - ukuku_prairie.connect(bay_water, FEATHER, one_way=True) # jesus jump - bay_water.connect(d5_entrance, FEATHER) # jesus jump into d5 entrance (wall clip), wall clip + jesus jump to get out - + left_bay_area.connect(bay_water, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump/rooster (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + animal_village.connect(bay_water, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump/rooster (can always reach bay_water with jesus jumping from every way to enter bay_water, so no one_way) + ukuku_prairie.connect(bay_water, OR(r.jesus_jump, r.jesus_rooster), one_way=True) # jesus jump/rooster + bay_water.connect(d5_entrance, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump/rooster into d5 entrance (wall clip), wall clip + jesus jump to get out + + prairie_island_seashell.connect(ukuku_prairie, AND(r.jesus_rooster, r.bush)) # jesus rooster from right side, screen transition on top of the water to reach the island + bay_madbatter_connector_exit.connect(bay_madbatter_connector_entrance, r.jesus_rooster, one_way=True) # jesus rooster (3 screen) through the underground passage leading to martha's bay mad batter + # fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, OR(FEATHER, SWORD, BOW), FLIPPERS)) # just swing/shoot at fisher, if photographer is on screen it is dumb + fisher_under_bridge.connect(bay_water, AND(TRADING_ITEM_FISHING_HOOK, FLIPPERS)) # face the fisherman from the left, get within 4 pixels (a range, not exact) of his left side, hold up, and mash a until you get the textbox. + + #TODO: add jesus rooster to trick list + + below_right_taltal.connect(next_to_castle, r.jesus_buffer, one_way=True) # face right, boots bonk and get far enough left to jesus buffer / boots bonk across the bottom wall to the stairs crow_gold_leaf.connect(castle_courtyard, BOMB) # bird on tree at left side kanalet, place a bomb against the tree and the crow flies off. With well placed second bomb the crow can be killed - mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, FEATHER)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect) - animal_village_bombcave_heartpiece.connect(animal_village_bombcave, PEGASUS_BOOTS) # boots bonk across bottom wall (both at entrance and in item room) - - d6_armos_island.connect(ukuku_prairie, FEATHER) # jesus jump (3 screen) from seashell mansion to armos island - armos_fairy_entrance.connect(d6_armos_island, PEGASUS_BOOTS, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling - d6_connector_right.connect(d6_connector_left, PEGASUS_BOOTS) # boots bonk across bottom wall at water and pits (can do both ways) - - obstacle_cave_entrance.connect(obstacle_cave_inside, OR(HOOKSHOT, AND(FEATHER, PEGASUS_BOOTS, OR(SWORD, MAGIC_ROD, BOW)))) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down - obstacle_cave_entrance.connect(obstacle_cave_inside, AND(PEGASUS_BOOTS, ROOSTER)) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall - d4_entrance.connect(below_right_taltal, FEATHER) # jesus jump a long way + mermaid_statue.connect(animal_village, AND(TRADING_ITEM_SCALE, r.super_jump_feather)) # early mermaid statue by buffering on top of the right ledge, then superjumping to the left (horizontal pixel perfect) + animal_village_connector_right.connect(animal_village_connector_left, r.shaq_jump) # shaq jump off the obstacle to get through + animal_village_connector_left.connect(animal_village_connector_right, r.hookshot_clip_block, one_way=True) # use hookshot with an enemy to clip through the obstacle + animal_village_bombcave_heartpiece.connect(animal_village_bombcave, r.pit_buffer_boots) # boots bonk across bottom wall (both at entrance and in item room) + + d6_armos_island.connect(ukuku_prairie, OR(r.jesus_jump, r.jesus_rooster)) # jesus jump / rooster (3 screen) from seashell mansion to armos island + armos_fairy_entrance.connect(d6_armos_island, r.jesus_buffer, one_way=True) # jesus jump from top (fairy bomb cave) to armos island with fast falling + d6_connector_right.connect(d6_connector_left, r.pit_buffer_boots) # boots bonk across bottom wall at water and pits (can do both ways) + d6_entrance.connect(ukuku_prairie, r.jesus_rooster, one_way=True) # jesus rooster (2 screen) from d6 entrance bottom ledge to ukuku prairie + d6_entrance.connect(armos_fairy_entrance, r.jesus_rooster, one_way=True) # jesus rooster (2 screen) from d6 entrance top ledge to armos fairy entrance + armos_fairy_entrance.connect(d6_armos_island, r.jesus_rooster, one_way=True) # jesus rooster from top (fairy bomb cave) to armos island + armos_fairy_entrance.connect(raft_exit, r.jesus_rooster) # jesus rooster (2-ish screen) from fairy cave to lower raft connector + + obstacle_cave_entrance.connect(obstacle_cave_inside, OR(r.hookshot_clip_block, r.shaq_jump)) # get past crystal rocks by hookshotting into top pushable block, or boots dashing into top wall where the pushable block is to superjump down + obstacle_cave_entrance.connect(obstacle_cave_inside, r.boots_roosterhop) # get past crystal rocks pushing the top pushable block, then boots dashing up picking up the rooster before bonking. Pause buffer until rooster is fully picked up then throw it down before bonking into wall + d4_entrance.connect(below_right_taltal, OR(r.jesus_jump, r.jesus_rooster), one_way=True) # jesus jump/rooster 5 screens to staircase below damp cave + lower_right_taltal.connect(below_right_taltal, OR(r.jesus_jump, r.jesus_rooster), one_way=True) # jesus jump/rooster to upper ledges, jump off, enter and exit s+q menu to regain pauses, then jesus jump 4 screens to staircase below damp cave + below_right_taltal.connect(outside_swim_cave, r.jesus_rooster) # jesus rooster into the cave entrance after jumping down the ledge, can jesus jump back to the ladder 1 screen below + outside_mambo.connect(below_right_taltal, OR(r.jesus_rooster, r.jesus_jump)) # jesus jump/rooster to mambo's cave entrance + if options.hardmode != "oracle": # don't take damage from drowning in water. Could get it with more health probably but standard 3 hearts is not enough + mambo.connect(inside_mambo, AND(OCARINA, r.bomb_trigger)) # while drowning, buffer a bomb and after it explodes, buffer another bomb out of the save and quit menu. + outside_raft_house.connect(below_right_taltal, r.jesus_rooster, one_way=True) # jesus rooster from the ledge at raft to the staircase 1 screen south + lower_right_taltal.connect(outside_multichest_left, r.jesus_rooster) # jesus rooster past staircase leading up the mountain + outside_rooster_house.connect(lower_right_taltal, r.jesus_rooster, one_way=True) # jesus rooster down to staircase below damp cave + if options.entranceshuffle in ("default", "simple"): # connector cave from armos d6 area to raft shop may not be randomized to add a flippers path since flippers stop you from jesus jumping - below_right_taltal.connect(raft_game, AND(FEATHER, r.attack_hookshot_powder), one_way=True) # jesus jump from heartpiece water cave, around the island and clip past the diagonal gap in the rock, then jesus jump all the way down the waterfall to the chests (attack req for hardlock flippers+feather scenario) - outside_raft_house.connect(below_right_taltal, AND(FEATHER, PEGASUS_BOOTS)) #superjump from ledge left to right, can buffer to land on ledge instead of water, then superjump right which is pixel perfect - bridge_seashell.connect(outside_rooster_house, AND(PEGASUS_BOOTS, POWER_BRACELET)) # boots bonk - bird_key.connect(bird_cave, AND(FEATHER, PEGASUS_BOOTS)) # boots jump above wall, use multiple pit buffers to get across - mountain_bridge_staircase.connect(outside_rooster_house, OR(PEGASUS_BOOTS, FEATHER)) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across - left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(PEGASUS_BOOTS, FEATHER), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left - left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(PEGASUS_BOOTS, SWORD, BOW, MAGIC_ROD)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area - + below_right_taltal.connect(raft_game, AND(OR(r.jesus_jump, r.jesus_rooster), r.attack_hookshot_powder), one_way=True) # jesus jump from heartpiece water cave, around the island and clip past the diagonal gap in the rock, then jesus jump all the way down the waterfall to the chests (attack req for hardlock flippers+feather scenario) + outside_raft_house.connect(below_right_taltal, AND(r.super_jump, PEGASUS_BOOTS)) #superjump from ledge left to right, can buffer to land on ledge instead of water, then superjump right which is pixel perfect. Boots to get out of wall after landing + bridge_seashell.connect(outside_rooster_house, AND(OR(r.hookshot_spam_pit, r.boots_bonk_pit), POWER_BRACELET)) # boots bonk or hookshot spam over the pit to get to the rock + bird_key.connect(bird_cave, AND(r.boots_jump, r.pit_buffer)) # boots jump above wall, use multiple pit buffers to get across + right_taltal_connector2.connect(right_taltal_connector3, r.pit_buffer_itemless, one_way=True) # 2 separate pit buffers so not obnoxious to get past the two pit rooms before d7 area. 2nd pits can pit buffer on top right screen, bottom wall to scroll on top of the wall on bottom screen + mountain_bridge_staircase.connect(outside_rooster_house, r.pit_buffer_boots) # cross bridge to staircase with pit buffer to clip bottom wall and jump or boots bonk across + left_right_connector_cave_entrance.connect(left_right_connector_cave_exit, AND(r.boots_jump, r.pit_buffer), one_way=True) # boots jump to bottom left corner of pits, pit buffer and jump to left + left_right_connector_cave_exit.connect(left_right_connector_cave_entrance, AND(ROOSTER, OR(r.boots_roosterhop, r.super_jump_rooster)), one_way=True) # pass through the passage in reverse using a boots rooster hop or rooster superjump in the one way passage area + + windfish.connect(nightmare, AND(SWORD, OR(BOOMERANG, BOW, BOMB, COUNT(SWORD, 2), AND(OCARINA, OR(SONG1, SONG3))))) # sword quick kill blob, can kill dethl with bombs or sword beams, and can use ocarina to freeze one of ganon's bats to skip dethl eye phase + self.start = start_house self.egg = windfish_egg self.nightmare = nightmare @@ -659,7 +718,7 @@ def __init__(self, outside, requirement, one_way_enter_requirement="UNSET", one_ self.requirement = requirement self.one_way_enter_requirement = one_way_enter_requirement self.one_way_exit_requirement = one_way_exit_requirement - + def addRequirement(self, new_requirement): self.requirement = OR(self.requirement, new_requirement) @@ -674,9 +733,9 @@ def addEnterRequirement(self, new_requirement): self.one_way_enter_requirement = new_requirement else: self.one_way_enter_requirement = OR(self.one_way_enter_requirement, new_requirement) - + def enterIsSet(self): return self.one_way_enter_requirement != "UNSET" - + def exitIsSet(self): return self.one_way_exit_requirement != "UNSET" diff --git a/worlds/ladx/LADXR/logic/requirements.py b/worlds/ladx/LADXR/logic/requirements.py index acc969ba938d..fa01627a15c3 100644 --- a/worlds/ladx/LADXR/logic/requirements.py +++ b/worlds/ladx/LADXR/logic/requirements.py @@ -254,17 +254,62 @@ def isConsumable(item) -> bool: class RequirementsSettings: def __init__(self, options): self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG) + self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG) - self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # switches, hinox, shrouded stalfos + self.attack_hookshot = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hinox, shrouded stalfos + self.hit_switch = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # hit switches in dungeons self.attack_hookshot_powder = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT, MAGIC_POWDER) # zols, keese, moldorm self.attack_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # ? self.attack_hookshot_no_bomb = OR(SWORD, BOW, MAGIC_ROD, BOOMERANG, HOOKSHOT) # vire self.attack_no_boomerang = OR(SWORD, BOMB, BOW, MAGIC_ROD, HOOKSHOT) # teleporting owls self.attack_skeleton = OR(SWORD, BOMB, BOW, BOOMERANG, HOOKSHOT) # cannot kill skeletons with the fire rod + self.attack_gibdos = OR(SWORD, BOMB, BOW, BOOMERANG, AND(MAGIC_ROD, HOOKSHOT)) # gibdos are only stunned with hookshot, but can be burnt to jumping stalfos first with magic rod + self.attack_pols_voice = OR(BOMB, MAGIC_ROD, AND(OCARINA, SONG1)) # BOW works, but isn't as reliable as it needs 4 arrows. + self.attack_wizrobe = OR(BOMB, MAGIC_ROD) # BOW works, but isn't as reliable as it needs 4 arrows. + self.stun_wizrobe = OR(BOOMERANG, MAGIC_POWDER, HOOKSHOT) self.rear_attack = OR(SWORD, BOMB) # mimic self.rear_attack_range = OR(MAGIC_ROD, BOW) # mimic self.fire = OR(MAGIC_POWDER, MAGIC_ROD) # torches self.push_hardhat = OR(SHIELD, SWORD, HOOKSHOT, BOOMERANG) + self.shuffled_magnifier = TRADING_ITEM_MAGNIFYING_GLASS # overwritten if vanilla trade items + + self.throw_pot = POWER_BRACELET # grab pots to kill enemies + self.throw_enemy = POWER_BRACELET # grab stunned enemies to kill enemies + self.tight_jump = FEATHER # jumps that are possible but are tight to make it across + self.super_jump = AND(FEATHER, OR(SWORD, BOW, MAGIC_ROD)) # standard superjump for glitch logic + self.super_jump_boots = AND(PEGASUS_BOOTS, FEATHER, OR(SWORD, BOW, MAGIC_ROD)) # boots dash into wall for unclipped superjump + self.super_jump_feather = FEATHER # using only feather to align and jump off walls + self.super_jump_sword = AND(FEATHER, SWORD) # unclipped superjumps + self.super_jump_rooster = AND(ROOSTER, OR(SWORD, BOW, MAGIC_ROD)) # use rooster instead of feather to superjump off walls (only where rooster is allowed to be used) + self.shaq_jump = FEATHER # use interactable objects (keyblocks / pushable blocks) + self.boots_superhop = AND(PEGASUS_BOOTS, OR(MAGIC_ROD, BOW)) # dash into walls, pause, unpause and use weapon + hold direction away from wall. Only works in peg rooms + self.boots_roosterhop = AND(PEGASUS_BOOTS, ROOSTER) # dash towards a wall, pick up the rooster and throw it away from the wall before hitting the wall to get a superjump + self.jesus_jump = FEATHER # pause on the frame of hitting liquid (water / lava) to be able to jump again on unpause + self.jesus_buffer = PEGASUS_BOOTS # use a boots bonk to get on top of liquid (water / lava), then use buffers to get into positions + self.damage_boost_special = options.hardmode == "none" # use damage to cross pits / get through forced barriers without needing an enemy that can be eaten by bowwow + self.damage_boost = (options.bowwow == "normal") & (options.hardmode == "none") # Use damage to cross pits / get through forced barriers + self.sideways_block_push = True # wall clip pushable block, get against the edge and push block to move it sideways + self.wall_clip = True # push into corners to get further into walls, to avoid collision with enemies along path (see swamp flowers for example) or just getting a better position for jumps + self.pit_buffer_itemless = True # walk on top of pits and buffer down + self.pit_buffer = FEATHER # jump on top of pits and buffer to cross vertical gaps + self.pit_buffer_boots = OR(PEGASUS_BOOTS, FEATHER) # use boots or feather to cross gaps + self.boots_jump = AND(PEGASUS_BOOTS, FEATHER) # use boots jumps to cross 4 gap spots or other hard to reach spots + self.boots_bonk = PEGASUS_BOOTS # bonk against walls in 2d sections to get to higher places (no pits involved usually) + self.boots_bonk_pit = PEGASUS_BOOTS # use boots bonks to cross 1 tile gaps + self.boots_bonk_2d_spikepit = AND(PEGASUS_BOOTS, "MEDICINE2") # use iframes from medicine to get a boots dash going in 2d spike pits (kanalet secret passage, d3 2d section to boss) + self.boots_bonk_2d_hell = PEGASUS_BOOTS # seperate boots bonks from hell logic which are harder? + self.boots_dash_2d = PEGASUS_BOOTS # use boots to dash over 1 tile gaps in 2d sections + self.hookshot_spam_pit = HOOKSHOT # use hookshot with spam to cross 1 tile gaps + self.hookshot_clip = AND(HOOKSHOT, options.superweapons == False) # use hookshot at specific angles to hookshot past blocks (see forest north log cave, dream shrine entrance for example) + self.hookshot_clip_block = HOOKSHOT # use hookshot spam with enemies to clip through entire blocks (d5 room before gohma, d2 pots room before boss) + self.hookshot_over_pit = HOOKSHOT # use hookshot while over a pit to reach certain areas (see d3 vacuum room, d5 north of crossroads for example) + self.hookshot_jump = AND(HOOKSHOT, FEATHER) # while over pits, on the first frame after the hookshot is retracted you can input a jump to cross big pit gaps + self.bookshot = AND(FEATHER, HOOKSHOT) # use feather on A, hookshot on B on the same frame to get a speedy hookshot that can be used to clip past blocks + self.bomb_trigger = BOMB # drop two bombs at the same time to trigger cutscenes or pickup items (can use pits, or screen transitions + self.shield_bump = SHIELD # use shield to knock back enemies or knock off enemies when used in combination with superjumps + self.text_clip = False & options.nagmessages # trigger a text box on keyblock or rock or obstacle while holding diagonal to clip into the side. Removed from logic for now + self.jesus_rooster = AND(ROOSTER, options.hardmode != "oracle") # when transitioning on top of water, buffer the rooster out of sq menu to spawn it. Then do an unbuffered pickup of the rooster as soon as you spawn again to pick it up + self.zoomerang = AND(PEGASUS_BOOTS, FEATHER, BOOMERANG) # after starting a boots dash, buffer boomerang (on b), feather and the direction you're dashing in to get boosted in certain directions self.boss_requirements = [ SWORD, # D1 boss @@ -282,7 +327,7 @@ def __init__(self, options): "HINOX": self.attack_hookshot, "DODONGO": BOMB, "CUE_BALL": SWORD, - "GHOMA": OR(BOW, HOOKSHOT), + "GHOMA": OR(BOW, HOOKSHOT, MAGIC_ROD, BOOMERANG), "SMASHER": POWER_BRACELET, "GRIM_CREEPER": self.attack_hookshot_no_bomb, "BLAINO": SWORD, @@ -293,9 +338,15 @@ def __init__(self, options): } # Adjust for options - if options.bowwow != 'normal': + if not options.tradequest: + self.shuffled_magnifier = True # completing trade quest not required + if options.hardmode == "ohko": + self.miniboss_requirements["ROLLING_BONES"] = OR(BOW, MAGIC_ROD, BOOMERANG, AND(FEATHER, self.attack_hookshot)) # should not deal with roller damage + if options.bowwow != "normal": # We cheat in bowwow mode, we pretend we have the sword, as bowwow can pretty much do all what the sword ca$ # Except for taking out bushes (and crystal pillars are removed) self.bush.remove(SWORD) + self.pit_bush.remove(SWORD) + self.hit_switch.remove(SWORD) if options.logic == "casual": # In casual mode, remove the more complex kill methods self.bush.remove(MAGIC_POWDER) @@ -305,14 +356,18 @@ def __init__(self, options): self.attack_hookshot_powder.remove(BOMB) self.attack_no_boomerang.remove(BOMB) self.attack_skeleton.remove(BOMB) - if options.logic == "hard": + + if options.logic == 'hard' or options.logic == 'glitched' or options.logic == 'hell': + self.boss_requirements[1] = AND(OR(SWORD, MAGIC_ROD, BOMB), POWER_BRACELET) # bombs + bracelet genie self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish self.boss_requirements[6] = OR(MAGIC_ROD, AND(BOMB, BOW), COUNT(SWORD, 2), AND(OR(SWORD, HOOKSHOT, BOW), SHIELD)) # evil eagle 3 cycle magic rod / bomb arrows / l2 sword, and bow kill - if options.logic == "glitched": - self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish + self.attack_pols_voice = OR(BOMB, MAGIC_ROD, AND(OCARINA, SONG1), AND(self.stun_wizrobe, self.throw_enemy, BOW)) # wizrobe stun has same req as pols voice stun + self.attack_wizrobe = OR(BOMB, MAGIC_ROD, AND(self.stun_wizrobe, self.throw_enemy, BOW)) + + if options.logic == 'glitched' or options.logic == 'hell': self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs + if options.logic == "hell": - self.boss_requirements[3] = AND(FLIPPERS, OR(SWORD, MAGIC_ROD, BOW, BOMB)) # bomb angler fish - self.boss_requirements[6] = OR(MAGIC_ROD, BOMB, BOW, HOOKSHOT, COUNT(SWORD, 2), AND(SWORD, SHIELD)) # evil eagle off screen kill or 3 cycle with bombs self.boss_requirements[7] = OR(MAGIC_ROD, COUNT(SWORD, 2)) # hot head sword beams + self.miniboss_requirements["GHOMA"] = OR(BOW, HOOKSHOT, MAGIC_ROD, BOOMERANG, AND(OCARINA, BOMB, OR(SONG1, SONG3))) # use bombs to kill gohma, with ocarina to get good timings self.miniboss_requirements["GIANT_BUZZ_BLOB"] = OR(MAGIC_POWDER, COUNT(SWORD,2)) # use sword beams to damage buzz blob diff --git a/worlds/ladx/LADXR/patches/bank34.py b/worlds/ladx/LADXR/patches/bank34.py index 31b9ca124436..e88727e868c6 100644 --- a/worlds/ladx/LADXR/patches/bank34.py +++ b/worlds/ladx/LADXR/patches/bank34.py @@ -75,7 +75,7 @@ def addBank34(rom, item_list): .notCavesA: add hl, de ret - """ + pkgutil.get_data(__name__, os.path.join("bank3e.asm", "message.asm")).decode().replace("\r", ""), 0x4000), fill_nop=True) + """ + pkgutil.get_data(__name__, "bank3e.asm/message.asm").decode().replace("\r", ""), 0x4000), fill_nop=True) nextItemLookup = ItemNameStringBufferStart nameLookup = { diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm index 57771c17b3ca..de237c86293b 100644 --- a/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm +++ b/worlds/ladx/LADXR/patches/bank3e.asm/chest.asm @@ -835,6 +835,7 @@ ItemSpriteTable: db $46, $1C ; NIGHTMARE_KEY8 db $46, $1C ; NIGHTMARE_KEY9 db $4C, $1C ; Toadstool + db $AE, $14 ; Guardian Acorn LargeItemSpriteTable: db $AC, $02, $AC, $22 ; heart piece @@ -874,6 +875,7 @@ LargeItemSpriteTable: db $D8, $0D, $DA, $0D ; TradeItem12 db $DC, $0D, $DE, $0D ; TradeItem13 db $E0, $0D, $E2, $0D ; TradeItem14 + db $14, $42, $14, $62 ; Piece Of Power ItemMessageTable: db $90, $3D, $89, $93, $94, $95, $96, $97, $98, $99, $9A, $9B, $9C, $9D, $D9, $A2 @@ -888,7 +890,7 @@ ItemMessageTable: ; $80 db $4F, $C8, $CA, $CB, $E2, $E3, $E4, $CC, $CD, $2A, $2B, $C9, $C9, $C9, $C9, $C9 db $C9, $C9, $C9, $C9, $C9, $C9, $B8, $44, $C9, $C9, $C9, $C9, $C9, $C9, $C9, $C9 - db $C9, $C9, $C9, $C9, $9D + db $C9, $C9, $C9, $C9, $9D, $C9 RenderDroppedKey: ;TODO: See EntityInitKeyDropPoint for a few special cases to unload. diff --git a/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm index 0c1bc9d699e4..c57ce2f81ccd 100644 --- a/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm +++ b/worlds/ladx/LADXR/patches/bank3e.asm/itemnames.asm @@ -170,7 +170,7 @@ ItemNamePointers: dw ItemNameNightmareKey8 dw ItemNameNightmareKey9 dw ItemNameToadstool - dw ItemNameNone ; 0x51 + dw ItemNameGuardianAcorn dw ItemNameNone ; 0x52 dw ItemNameNone ; 0x53 dw ItemNameNone ; 0x54 @@ -254,6 +254,7 @@ ItemNamePointers: dw ItemTradeQuest12 dw ItemTradeQuest13 dw ItemTradeQuest14 + dw ItemPieceOfPower ItemNameNone: db m"NONE", $ff @@ -418,6 +419,8 @@ ItemNameNightmareKey9: db m"Got the {NIGHTMARE_KEY9}", $ff ItemNameToadstool: db m"Got the {TOADSTOOL}", $ff +ItemNameGuardianAcorn: + db m"Got a Guardian Acorn", $ff ItemNameHeartPiece: db m"Got the {HEART_PIECE}", $ff @@ -496,5 +499,8 @@ ItemTradeQuest13: db m"You've got the Scale", $ff ItemTradeQuest14: db m"You've got the Magnifying Lens", $ff + +ItemPieceOfPower: + db m"You've got a Piece of Power", $ff MultiNamePointers: \ No newline at end of file diff --git a/worlds/ladx/LADXR/patches/bank3e.py b/worlds/ladx/LADXR/patches/bank3e.py index 7e690349a335..632fffa7e63e 100644 --- a/worlds/ladx/LADXR/patches/bank3e.py +++ b/worlds/ladx/LADXR/patches/bank3e.py @@ -56,7 +56,7 @@ def addBank3E(rom, seed, player_id, player_name_list): """)) def get_asm(name): - return pkgutil.get_data(__name__, os.path.join("bank3e.asm", name)).decode().replace("\r", "") + return pkgutil.get_data(__name__, "bank3e.asm/" + name).decode().replace("\r", "") rom.patch(0x3E, 0x0000, 0x2F00, ASM(""" call MainJumpTable diff --git a/worlds/ladx/LADXR/patches/droppedKey.py b/worlds/ladx/LADXR/patches/droppedKey.py index d24b8b76c7a9..7853712a114a 100644 --- a/worlds/ladx/LADXR/patches/droppedKey.py +++ b/worlds/ladx/LADXR/patches/droppedKey.py @@ -24,14 +24,10 @@ def fixDroppedKey(rom): ld a, $06 ; giveItemMultiworld rst 8 - ldh a, [$F1] ; Load active sprite variant to see if this is just a normal small key - cp $1A - jr z, isAKey - - ;Show message (if not a key) + ;Show message ld a, $0A ; showMessageMultiworld rst 8 -isAKey: + ret """)) rom.patch(0x03, 0x24B7, "3E", "3E") # sanity check 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/LADXR/patches/maptweaks.py b/worlds/ladx/LADXR/patches/maptweaks.py index c25dd83dcada..8a5171b3540d 100644 --- a/worlds/ladx/LADXR/patches/maptweaks.py +++ b/worlds/ladx/LADXR/patches/maptweaks.py @@ -25,3 +25,16 @@ def addBetaRoom(rom): re.store(rom) rom.room_sprite_data_indoor[0x0FC] = rom.room_sprite_data_indoor[0x1A1] + + +def tweakBirdKeyRoom(rom): + # Make the bird key accessible without the rooster + re = RoomEditor(rom, 0x27A) + re.removeObject(1, 6) + re.removeObject(2, 6) + re.removeObject(3, 5) + re.removeObject(3, 6) + re.moveObject(1, 5, 2, 6) + re.moveObject(2, 5, 3, 6) + re.addEntity(3, 5, 0x9D) + re.store(rom) diff --git a/worlds/ladx/LADXR/patches/owl.py b/worlds/ladx/LADXR/patches/owl.py index 47e575191a31..20b8221604c6 100644 --- a/worlds/ladx/LADXR/patches/owl.py +++ b/worlds/ladx/LADXR/patches/owl.py @@ -81,23 +81,23 @@ def removeOwlEvents(rom): ; Give powder ld a, [$DB4C] - cp $10 + cp $20 jr nc, doNotGivePowder - ld a, $10 + ld a, $20 ld [$DB4C], a doNotGivePowder: ld a, [$DB4D] - cp $10 + cp $30 jr nc, doNotGiveBombs - ld a, $10 + ld a, $30 ld [$DB4D], a doNotGiveBombs: ld a, [$DB45] - cp $10 + cp $30 jr nc, doNotGiveArrows - ld a, $10 + ld a, $30 ld [$DB45], a doNotGiveArrows: diff --git a/worlds/ladx/LADXR/patches/songs.py b/worlds/ladx/LADXR/patches/songs.py index 59ca01c4c8c4..b080cf06bc92 100644 --- a/worlds/ladx/LADXR/patches/songs.py +++ b/worlds/ladx/LADXR/patches/songs.py @@ -72,6 +72,10 @@ def upgradeMarin(rom): rst 8 """), fill_nop=True) + # Load marin singing even if you have the marin date + rom.patch(0x03, 0x0A91, ASM("jp nz, $3F8D"), "", fill_nop=True) + rom.patch(0x05, 0x0E6E, ASM("jp nz, $7B4B"), "", fill_nop=True) + def upgradeManbo(rom): # Instead of checking if we have the song, check if we have a specific room flag set diff --git a/worlds/ladx/LADXR/patches/tradeSequence.py b/worlds/ladx/LADXR/patches/tradeSequence.py index 5b608977f20d..0eb46ae23ae2 100644 --- a/worlds/ladx/LADXR/patches/tradeSequence.py +++ b/worlds/ladx/LADXR/patches/tradeSequence.py @@ -1,7 +1,7 @@ from ..assembler import ASM -def patchTradeSequence(rom, boomerang_option): +def patchTradeSequence(rom, settings): patchTrendy(rom) patchPapahlsWife(rom) patchYipYip(rom) @@ -16,7 +16,7 @@ def patchTradeSequence(rom, boomerang_option): patchMermaid(rom) patchMermaidStatue(rom) patchSharedCode(rom) - patchVarious(rom, boomerang_option) + patchVarious(rom, settings) patchInventoryMenu(rom) @@ -265,8 +265,11 @@ def patchMermaidStatue(rom): and $10 ; scale ret z ldh a, [$F8] - and $20 + and $20 ; ROOM_STATUS_EVENT_2 ret nz + + ld hl, wTradeSequenceItem2 + res 4, [hl] ; take the trade item """), fill_nop=True) @@ -317,7 +320,7 @@ def patchSharedCode(rom): rom.patch(0x07, 0x3F7F, "00" * 7, ASM("ldh a, [$F8]\nor $20\nldh [$F8], a\nret")) -def patchVarious(rom, boomerang_option): +def patchVarious(rom, settings): # Make the zora photo work with the magnifier rom.patch(0x18, 0x09F3, 0x0A02, ASM(""" ld a, [wTradeSequenceItem2] @@ -330,22 +333,71 @@ def patchVarious(rom, boomerang_option): jp z, $3F8D ; UnloadEntity """), fill_nop=True) # Mimic invisibility - rom.patch(0x18, 0x2AC8, 0x2ACE, "", fill_nop=True) + rom.patch(0x19, 0x2AC0, ASM(""" + cp $97 + jr z, mermaidStatueCave + cp $98 + jr nz, visible + mermaidStatueCave: + ld a, [$DB7F] + and a + jr nz, 6 + visible: + """), ASM(""" + dec a ; save one byte by only doing one cp + or $01 + cp $97 + jr nz, visible + mermaidStatueCave: + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + jr z, 6 + visible: + """)) + # Zol invisibility + rom.patch(0x06, 0x3BE9, ASM(""" + cp $97 + jr z, mermaidStatueCave + cp $98 + ret nz ; visible + mermaidStatueCave: + ld a, [$DB7F] + and a + ret z + """), ASM(""" + dec a ; save one byte by only doing one cp + or $01 + cp $97 + ret nz ; visible + mermaidStatueCave: + ld a, [wTradeSequenceItem2] + and $20 ; MAGNIFYING_GLASS + ret nz + """)) # Ignore trade quest state for marin at beach rom.patch(0x18, 0x219E, 0x21A6, "", fill_nop=True) # Shift the magnifier 8 pixels rom.patch(0x03, 0x0F68, 0x0F6F, ASM(""" ldh a, [$F6] ; map room - cp $97 ; check if we are in the maginfier room + cp $97 ; check if we are in the magnifier room jp z, $4F83 """), fill_nop=True) # Something with the photographer rom.patch(0x36, 0x0948, 0x0950, "", fill_nop=True) - if boomerang_option not in {'trade', 'gift'}: # Boomerang cave is not patched, so adjust it + # Boomerang trade guy + # if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}: + if settings.tradequest: + # Update magnifier checks rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout - rom.patch(0x19, 0x05F4, 0x05FB, "", fill_nop=True) + else: + # Monkey bridge patch, always have the bridge there. + rom.patch(0x00, 0x333D, ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) + # Always have the boomerang trade guy enabled (magnifier not needed) + rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # show the guy + rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E"), ASM("ld a, $0E\ncp $0E"), fill_nop=True) # load the proper room layout + rom.patch(0x19, 0x05F4, ASM("ld a, [wTradeSequenceItem2]\nand a"), ASM("xor a"), fill_nop=True) def patchInventoryMenu(rom): diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index f29355f2ba86..8670738e0869 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -1,5 +1,5 @@ from BaseClasses import Region, Entrance, Location, CollectionState - +import typing from .LADXR.checkMetadata import checkMetadataTable from .Common import * @@ -25,6 +25,39 @@ def meta_to_name(meta): return f"{meta.name} ({meta.area})" +def get_location_name_groups() -> typing.Dict[str, typing.Set[str]]: + groups = { + "Instrument Pedestals": { + "Full Moon Cello (Tail Cave)", + "Conch Horn (Bottle Grotto)", + "Sea Lily's Bell (Key Cavern)", + "Surf Harp (Angler's Tunnel)", + "Wind Marimba (Catfish's Maw)", + "Coral Triangle (Face Shrine)", + "Organ of Evening Calm (Eagle's Tower)", + "Thunder Drum (Turtle Rock)", + }, + "Boss Rewards": { + "Moldorm Heart Container (Tail Cave)", + "Genie Heart Container (Bottle Grotto)", + "Slime Eye Heart Container (Key Cavern)", + "Angler Fish Heart Container (Angler's Tunnel)", + "Slime Eel Heart Container (Catfish's Maw)", + "Facade Heart Container (Face Shrine)", + "Evil Eagle Heart Container (Eagle's Tower)", + "Hot Head Heart Container (Turtle Rock)", + "Tunic Fairy Item 1 (Color Dungeon)", + "Tunic Fairy Item 2 (Color Dungeon)", + }, + } + # Add region groups + for s, v in checkMetadataTable.items(): + if s == "None": + continue + groups.setdefault(v.area, set()).add(meta_to_name(v)) + return groups + +links_awakening_location_name_groups = get_location_name_groups() def get_locations_to_id(): ret = { diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index c5dcc080537c..17052659157f 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -3,7 +3,7 @@ import os.path import typing import logging -from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup +from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup, Removed from collections import defaultdict import Utils @@ -58,7 +58,7 @@ class TextShuffle(DefaultOffToggle): class Rooster(DefaultOnToggle, LADXROption): """ [On] Adds the rooster to the item pool. - [Off] The rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means. + [Off] The rooster spot is still a check giving an item. But you will never find the rooster. In that case, any rooster spot is accessible without rooster by other means. """ display_name = "Rooster" ladxr_name = "rooster" @@ -486,20 +486,37 @@ def to_ladxr_option(self, all_options): return self.ladxr_name, s -class WarpImprovements(DefaultOffToggle): +class Warps(Choice): """ - [On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. - [Off] No change + [Improved] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. + [Improved Additional] Improved warps, and adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower. """ - display_name = "Warp Improvements" + display_name = "Warps" + option_vanilla = 0 + option_improved = 1 + option_improved_additional = 2 + default = option_vanilla + + +class InGameHints(DefaultOnToggle): + """ + When enabled, owl statues and library books may indicate the location of your items in the multiworld. + """ + display_name = "In-game Hints" + -class AdditionalWarpPoints(DefaultOffToggle): +class ForeignItemIcons(Choice): """ - [On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower - [Off] No change + Choose how to display foreign items. + [Guess By Name] Foreign items can look like any Link's Awakening item. + [Indicate Progression] Foreign items are either a Piece of Power (progression) or Guardian Acorn (non-progression). """ - display_name = "Additional Warp Points" + display_name = "Foreign Item Icons" + option_guess_by_name = 0 + option_indicate_progression = 1 + default = option_guess_by_name + ladx_option_groups = [ OptionGroup("Goal Options", [ @@ -515,13 +532,13 @@ class AdditionalWarpPoints(DefaultOffToggle): ShuffleStoneBeaks ]), OptionGroup("Warp Points", [ - WarpImprovements, - AdditionalWarpPoints, + Warps, ]), OptionGroup("Miscellaneous", [ TradeQuest, Rooster, TrendyGame, + InGameHints, NagMessages, BootsControls ]), @@ -533,6 +550,7 @@ class AdditionalWarpPoints(DefaultOffToggle): LinkPalette, Palette, TextShuffle, + ForeignItemIcons, APTitleScreen, GfxMod, Music, @@ -562,12 +580,12 @@ class LinksAwakeningOptions(PerGameCommonOptions): # 'bowwow': Bowwow, # 'overworld': Overworld, link_palette: LinkPalette - warp_improvements: WarpImprovements - additional_warp_points: AdditionalWarpPoints + warps: Warps trendy_game: TrendyGame gfxmod: GfxMod palette: Palette text_shuffle: TextShuffle + foreign_item_icons: ForeignItemIcons shuffle_nightmare_keys: ShuffleNightmareKeys shuffle_small_keys: ShuffleSmallKeys shuffle_maps: ShuffleMaps @@ -579,3 +597,7 @@ class LinksAwakeningOptions(PerGameCommonOptions): nag_messages: NagMessages ap_title_screen: APTitleScreen boots_controls: BootsControls + in_game_hints: InGameHints + + warp_improvements: Removed + additional_warp_points: Removed 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 21876ed671e2..8496d4cf49e3 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -4,6 +4,7 @@ import pkgutil import tempfile import typing +import re import bsdiff4 @@ -12,8 +13,10 @@ from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World from .Common import * +from . import ItemIconGuessing from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData, - ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name) + ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name, + links_awakening_item_name_groups) from .LADXR import generator from .LADXR.itempool import ItemPool as LADXRItemPool from .LADXR.locations.constants import CHEST_ITEMS @@ -23,7 +26,8 @@ from .LADXR.settings import Settings as LADXRSettings from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, - create_regions_from_ladxr, get_locations_to_id) + create_regions_from_ladxr, get_locations_to_id, + links_awakening_location_name_groups) from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups from .Rom import LADXDeltaPatch, get_base_rom_path @@ -66,6 +70,15 @@ class LinksAwakeningWebWorld(WebWorld): )] theme = "dirt" option_groups = ladx_option_groups + options_presets: typing.Dict[str, typing.Dict[str, typing.Any]] = { + "Keysanity": { + "shuffle_nightmare_keys": "any_world", + "shuffle_small_keys": "any_world", + "shuffle_maps": "any_world", + "shuffle_compasses": "any_world", + "shuffle_stone_beaks": "any_world", + } + } class LinksAwakeningWorld(World): """ @@ -98,9 +111,9 @@ class LinksAwakeningWorld(World): # Items can be grouped using their names to allow easy checking if any item # from that group has been collected. Group names can also be used for !hint - #item_name_groups = { - # "weapons": {"sword", "lance"} - #} + item_name_groups = links_awakening_item_name_groups + + location_name_groups = links_awakening_location_name_groups prefill_dungeon_items = None @@ -213,7 +226,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) @@ -369,66 +382,36 @@ def priority(item): name_cache = {} # Tries to associate an icon from another game with an icon we have - def guess_icon_for_other_world(self, other): + def guess_icon_for_other_world(self, foreign_item): if not self.name_cache: - forbidden = [ - "TRADING", - "ITEM", - "BAD", - "SINGLE", - "UPGRADE", - "BLUE", - "RED", - "NOTHING", - "MESSAGE", - ] for item in ladxr_item_to_la_item_name.keys(): self.name_cache[item] = item splits = item.split("_") - self.name_cache["".join(splits)] = item - if 'RUPEES' in splits: - self.name_cache["".join(reversed(splits))] = item - for word in item.split("_"): - if word not in forbidden and not word.isnumeric(): + if word not in ItemIconGuessing.BLOCKED_ASSOCIATIONS and not word.isnumeric(): self.name_cache[word] = item - others = { - 'KEY': 'KEY', - 'COMPASS': 'COMPASS', - 'BIGKEY': 'NIGHTMARE_KEY', - 'MAP': 'MAP', - 'FLUTE': 'OCARINA', - 'SONG': 'OCARINA', - 'MUSHROOM': 'TOADSTOOL', - 'GLOVE': 'POWER_BRACELET', - 'BOOT': 'PEGASUS_BOOTS', - 'SHOE': 'PEGASUS_BOOTS', - 'SHOES': 'PEGASUS_BOOTS', - 'SANCTUARYHEARTCONTAINER': 'HEART_CONTAINER', - 'BOSSHEARTCONTAINER': 'HEART_CONTAINER', - 'HEARTCONTAINER': 'HEART_CONTAINER', - 'ENERGYTANK': 'HEART_CONTAINER', - 'MISSILE': 'SINGLE_ARROW', - 'BOMBS': 'BOMB', - 'BLUEBOOMERANG': 'BOOMERANG', - 'MAGICMIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', - 'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS', - 'MESSAGE': 'TRADING_ITEM_LETTER', - # TODO: Also use AP item name - } - for name in others.values(): + for name in ItemIconGuessing.SYNONYMS.values(): assert name in self.name_cache, name assert name in CHEST_ITEMS, name - self.name_cache.update(others) - - - uppered = other.upper() - if "BIG KEY" in uppered: - return 'NIGHTMARE_KEY' - possibles = other.upper().split(" ") - rejoined = "".join(possibles) - if rejoined in self.name_cache: - return self.name_cache[rejoined] + self.name_cache.update(ItemIconGuessing.SYNONYMS) + pluralizations = {k + "S": v for k, v in self.name_cache.items()} + self.name_cache = pluralizations | self.name_cache + + uppered = foreign_item.name.upper() + foreign_game = self.multiworld.game[foreign_item.player] + phrases = ItemIconGuessing.PHRASES.copy() + if foreign_game in ItemIconGuessing.GAME_SPECIFIC_PHRASES: + phrases.update(ItemIconGuessing.GAME_SPECIFIC_PHRASES[foreign_game]) + + for phrase, icon in phrases.items(): + if phrase in uppered: + return icon + # pattern for breaking down camelCase, also separates out digits + pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=\d)") + possibles = pattern.sub(' ', foreign_item.name).upper() + for ch in "[]()_": + possibles = possibles.replace(ch, " ") + possibles = possibles.split() for name in possibles: if name in self.name_cache: return self.name_cache[name] @@ -454,8 +437,15 @@ def generate_output(self, output_directory: str): # If the item name contains "sword", use a sword icon, etc # Otherwise, use a cute letter as the icon + elif self.options.foreign_item_icons == 'guess_by_name': + loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item) + loc.ladxr_item.custom_item_name = loc.item.name + else: - loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item.name) + if loc.item.advancement: + loc.ladxr_item.item = 'PIECE_OF_POWER' + else: + loc.ladxr_item.item = 'GUARDIAN_ACORN' loc.ladxr_item.custom_item_name = loc.item.name if loc.item: @@ -509,3 +499,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/ladx/test/TestDungeonLogic.py b/worlds/ladx/test/TestDungeonLogic.py index b9b9672b9b16..3202afa95bc1 100644 --- a/worlds/ladx/test/TestDungeonLogic.py +++ b/worlds/ladx/test/TestDungeonLogic.py @@ -10,7 +10,7 @@ class TestD6(LADXTestBase): def test_keylogic(self): keys = self.get_items_by_name(ItemName.KEY6) - self.collect_by_name([ItemName.FACE_KEY, ItemName.HOOKSHOT, ItemName.POWER_BRACELET, ItemName.BOMB, ItemName.FEATHER, ItemName.FLIPPERS]) + self.collect_by_name([ItemName.FACE_KEY, ItemName.HOOKSHOT, ItemName.POWER_BRACELET, ItemName.BOMB, ItemName.PEGASUS_BOOTS, ItemName.FEATHER, ItemName.FLIPPERS]) # Can reach an un-keylocked item in the dungeon self.assertTrue(self.can_reach_location("L2 Bracelet Chest (Face Shrine)")) @@ -18,18 +18,18 @@ def test_keylogic(self): location_1 = "Tile Room Key (Face Shrine)" location_2 = "Top Right Horse Heads Chest (Face Shrine)" location_3 = "Pot Locked Chest (Face Shrine)" - self.assertFalse(self.can_reach_location(location_1)) - self.assertFalse(self.can_reach_location(location_2)) - self.assertFalse(self.can_reach_location(location_3)) + self.assertFalse(self.can_reach_location(location_1), "Tile Room Key, 0 keys") + self.assertFalse(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 0 keys") + self.assertFalse(self.can_reach_location(location_3), "Pot Locked Chest, 0 keys") self.collect(keys[0]) - self.assertTrue(self.can_reach_location(location_1)) - self.assertFalse(self.can_reach_location(location_2)) - self.assertFalse(self.can_reach_location(location_3)) + self.assertTrue(self.can_reach_location(location_1), "Tile Room Key, 1 key") + self.assertFalse(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 1 key") + self.assertFalse(self.can_reach_location(location_3), "Pot Locked Chest, 1 key") self.collect(keys[1]) - self.assertTrue(self.can_reach_location(location_1)) - self.assertTrue(self.can_reach_location(location_2)) - self.assertFalse(self.can_reach_location(location_3)) + self.assertTrue(self.can_reach_location(location_1), "Tile Room Key, 2 keys") + self.assertTrue(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 2 keys") + self.assertFalse(self.can_reach_location(location_3), "Pot Locked Chest, 2 keys") self.collect(keys[2]) - self.assertTrue(self.can_reach_location(location_1)) - self.assertTrue(self.can_reach_location(location_2)) - self.assertTrue(self.can_reach_location(location_3)) + self.assertTrue(self.can_reach_location(location_1), "Tile Room Key, 3 keys") + self.assertTrue(self.can_reach_location(location_2), "Top Right Horse Heads Chest, 3 keys") + self.assertTrue(self.can_reach_location(location_3), "Pot Locked Chest, 3 keys") diff --git a/worlds/ladx/test/__init__.py b/worlds/ladx/test/__init__.py index 0e616ac557d0..059a09b0728d 100644 --- a/worlds/ladx/test/__init__.py +++ b/worlds/ladx/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from ..Common import LINKS_AWAKENING class LADXTestBase(WorldTestBase): game = LINKS_AWAKENING diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index b0148269eab3..0fe63526c63b 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -34,7 +34,7 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n for data in WORLD_PATHS_JSON: if "requiredNodes" in data: regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]]) - regions_with_entrance_checks = list(set(regions_with_entrance_checks)) + regions_with_entrance_checks = sorted(set(regions_with_entrance_checks)) for region_id in regions_with_entrance_checks: region = regions_table[region_id] location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event") diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index 2b3dc41239c3..8463e56e54c1 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -38,7 +38,7 @@ class LandstalkerWorld(World): item_name_to_id = build_item_name_to_id_table() location_name_to_id = build_location_name_to_id_table() - cached_spheres: ClassVar[List[Set[Location]]] + cached_spheres: List[Set[Location]] def __init__(self, multiworld, player): super().__init__(multiworld, player) @@ -47,6 +47,7 @@ def __init__(self, multiworld, player): self.dark_region_ids = [] self.teleport_tree_pairs = [] self.jewel_items = [] + self.cached_spheres = [] def fill_slot_data(self) -> dict: # Generate hints. @@ -220,14 +221,17 @@ def get_starting_health(self): return 4 @classmethod - def stage_post_fill(cls, multiworld): + def stage_post_fill(cls, multiworld: MultiWorld): # Cache spheres for hint calculation after fill completes. - cls.cached_spheres = list(multiworld.get_spheres()) + cached_spheres = list(multiworld.get_spheres()) + for world in multiworld.get_game_worlds(cls.game): + world.cached_spheres = cached_spheres @classmethod - def stage_modify_multidata(cls, *_): + def stage_modify_multidata(cls, multiworld: MultiWorld, *_): # Clean up all references in cached spheres after generation completes. - del cls.cached_spheres + for world in multiworld.get_game_worlds(cls.game): + world.cached_spheres = [] def adjust_shop_prices(self): # Calculate prices for items in shops once all items have their final position diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 8d6a7fc4ebee..2a61a71f5fce 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -3,13 +3,13 @@ """ from logging import warning -from BaseClasses import Item, ItemClassification, Tutorial +from BaseClasses import CollectionState, Item, ItemClassification, Tutorial from Options import OptionError from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP -from .options import LingoOptions, lingo_option_groups +from .options import LingoOptions, lingo_option_groups, SunwarpAccess, VictoryCondition from .player_logic import LingoPlayerLogic from .regions import create_regions @@ -54,20 +54,54 @@ class LingoWorld(World): player_logic: LingoPlayerLogic def generate_early(self): - if not (self.options.shuffle_doors or self.options.shuffle_colors or self.options.shuffle_sunwarps): + if not (self.options.shuffle_doors or self.options.shuffle_colors or + (self.options.sunwarp_access >= SunwarpAccess.option_unlock and + self.options.victory_condition == VictoryCondition.option_pilgrimage)): if self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression" - f" items. Please turn on Door Shuffle, Color Shuffle, or Sunwarp Shuffle if that doesn't seem" - f" right.") + warning(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on Door" + f" Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage victory condition" + f" if that doesn't seem right.") else: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any" - f" progression items. Please turn on Door Shuffle, Color Shuffle or Sunwarp Shuffle.") + raise OptionError(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on" + f" Door Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage" + f" victory condition.") self.player_logic = LingoPlayerLogic(self) def create_regions(self): create_regions(self) + if not self.options.shuffle_postgame: + state = CollectionState(self.multiworld) + state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True) + + # Note: relies on the assumption that real_items is a definitive list of real progression items in this + # world, and is not modified after being created. + for item in self.player_logic.real_items: + state.collect(self.create_item(item), True) + + # Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway. + if self.player_logic.forced_good_item != "": + state.collect(self.create_item(self.player_logic.forced_good_item), True) + + all_locations = self.multiworld.get_locations(self.player) + state.sweep_for_advancements(locations=all_locations) + + unreachable_locations = [location for location in all_locations + if not state.can_reach_location(location.name, self.player)] + + for location in unreachable_locations: + if location.name in self.player_logic.event_loc_to_item.keys(): + continue + + self.player_logic.real_locations.remove(location.name) + location.parent_region.locations.remove(location) + + if len(self.player_logic.real_items) > len(self.player_logic.real_locations): + raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number" + f" of required items without shuffling the postgame. Either enable postgame" + f" shuffling, or choose different options.") + def create_items(self): pool = [self.create_item(name) for name in self.player_logic.real_items] @@ -136,7 +170,8 @@ def fill_slot_data(self): slot_options = [ "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", "enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks", - "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps" + "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps", + "group_doors" ] slot_data = { diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 4d6771a7350d..3783b68af98c 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1,6 +1,13 @@ --- # This file is an associative array where the keys are region names. Rooms - # have four properties: entrances, panels, doors, and paintings. + # have a number of properties: + # - entrances + # - panels + # - doors + # - panel_doors + # - paintings + # - progression + # - sunwarps # # entrances is an array of regions from which this room can be accessed. The # key of each entry is the room that can access this one. The value is a list @@ -13,7 +20,7 @@ # room that the door is in. The room name may be omitted if the door is # located in the current room. # - # panels is an array of panels in the room. The key of the array is an + # panels is a named array of panels in the room. The key of the array is an # arbitrary name for the panel. Panels can have the following fields: # - id: The internal ID of the panel in the LINGO map # - required_room: In addition to having access to this room, the player must @@ -45,7 +52,7 @@ # - hunt: If True, the tracker will show this panel even when it is # not a check. Used for hunts like the Number Hunt. # - # doors is an array of doors associated with this room. When door + # doors is a named array of doors associated with this room. When door # randomization is enabled, each of these is an item. The key is a name that # will be displayed as part of the item's name. Doors can have the following # fields: @@ -78,6 +85,18 @@ # - event: Denotes that the door is event only. This is similar to # setting both skip_location and skip_item. # + # panel_doors is a named array of "panel doors" associated with this room. + # When panel door shuffle is enabled, each of these becomes an item, and those + # items block access to the listed panels. The key is a name for internal + # reference only. Panel doors can have the following fields: + # - panels: Required. This is the set of panels that are blocked by this + # panel door. + # - item_name: Overrides the name of the item generated for this panel + # door. If not specified, the item name will be generated from + # the room name and the name(s) of the panel(s). + # - panel_group: When region grouping is enabled, all panel doors with the + # same group will be covered by a single item. + # # paintings is an array of paintings in the room. This is used for painting # shuffling. # - id: The internal painting ID from the LINGO map. @@ -105,6 +124,14 @@ # fine in door shuffle mode. # - move: Denotes that the painting is able to move. # + # progression is a named array of items that define an ordered set of items. + # progression items do not have any true connection to the rooms that they + # are defined in, but it is best to place them in a thematically appropriate + # room. The key for a progression entry is the name of the item that will be + # created. A progression entry is a dictionary with one or both of a "doors" + # key and a "panel_doors" key. These fields should be lists of doors or + # panel doors that will be contained in this progressive item. + # # sunwarps is an array of sunwarps in the room. This is used for sunwarp # shuffling. # - dots: The number of dots on this sunwarp. @@ -140,6 +167,15 @@ painting: True The Colorful: painting: True + Welcome Back Area: + room: Welcome Back Area + door: Shortcut to Starting Room + Second Room: + door: Main Door + Hidden Room: + door: Back Right Door + Rhyme Room (Looped Square): + door: Rhyme Room Entrance panels: HI: id: Entry Room/Panel_hi_hi @@ -184,6 +220,10 @@ panel: RACECAR (Black) - room: The Tenacious panel: SOLOS (Black) + panel_doors: + HIDDEN: + panels: + - HIDDEN paintings: - id: arrows_painting exit_only: True @@ -294,6 +334,10 @@ panel: SOLOS (Black) - room: Hub Room panel: RAT + panel_doors: + OPEN: + panels: + - OPEN paintings: - id: owl_painting orientation: north @@ -308,7 +352,13 @@ panels: Achievement: id: Countdown Panels/Panel_seeker_seeker - required_room: Hidden Room + # The Seeker uniquely has the property that 1) it can be entered (through the Pilgrim Room) without opening the + # front door in panels mode door shuffle, and 2) the front door panel is part of the CDP. This necessitates this + # required_panel clause, because the entrance panel needs to be solvable for the achievement even if an + # alternate entrance to the room is used. + required_panel: + room: Hidden Room + panel: OPEN tag: forbid check: True achievement: The Seeker @@ -432,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 @@ -528,6 +580,23 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + ORDER: + panels: + - ORDER + SLAUGHTER: + panel_group: Tenacious Entrance Panels + panels: + - SLAUGHTER + TRACE: + panels: + - TRACE + RAT: + panels: + - RAT + OPEN: + panels: + - OPEN paintings: - id: maze_painting orientation: west @@ -599,12 +668,13 @@ item_name: "6 Sunwarp" progression: Progressive Pilgrimage: - - 1 Sunwarp - - 2 Sunwarp - - 3 Sunwarp - - 4 Sunwarp - - 5 Sunwarp - - 6 Sunwarp + doors: + - 1 Sunwarp + - 2 Sunwarp + - 3 Sunwarp + - 4 Sunwarp + - 5 Sunwarp + - 6 Sunwarp Pilgrim Antechamber: # The entrances to this room are special. When pilgrimage is enabled, we use a special access rule to determine # whether a pilgrimage can succeed. When pilgrimage is disabled, the sun painting will be added to the pool. @@ -870,6 +940,26 @@ panel: DRAWL + RUNS - room: Owl Hallway panel: READS + RUST + - room: Ending Area + panel: THE END + panel_doors: + DECAY: + panel_group: Tenacious Entrance Panels + panels: + - DECAY + NOPE: + panels: + - NOPE + WE ROT: + panels: + - WE ROT + WORDS SWORD: + panels: + - WORDS + - SWORD + BEND HI: + panels: + - BEND HI paintings: - id: eye_painting disable: True @@ -884,6 +974,14 @@ direction: exit entrance_indicator_pos: [ -17, 2.5, -41.01 ] orientation: north + progression: + Progressive Suits Area: + panel_doors: + - WORDS SWORD + - room: Lost Area + panel_door: LOST + - room: Amen Name Area + panel_door: AMEN NAME Lost Area: entrances: Outside The Agreeable: @@ -909,6 +1007,11 @@ panels: - LOST (1) - LOST (2) + panel_doors: + LOST: + panels: + - LOST (1) + - LOST (2) Amen Name Area: entrances: Crossroads: @@ -942,6 +1045,11 @@ panels: - AMEN - NAME + panel_doors: + AMEN NAME: + panels: + - AMEN + - NAME Suits Area: entrances: Amen Name Area: @@ -1045,6 +1153,13 @@ - LEVEL (White) - RACECAR (White) - SOLOS (White) + panel_doors: + Black Palindromes: + item_name: The Tenacious - Black Palindromes (Panels) + panels: + - LEVEL (Black) + - RACECAR (Black) + - SOLOS (Black) Near Far Area: entrances: Hub Room: True @@ -1070,6 +1185,21 @@ panels: - NEAR - FAR + panel_doors: + NEAR FAR: + item_name: Symmetry Room - NEAR, FAR (Panels) + panel_group: Symmetry Room Panels + panels: + - NEAR + - FAR + progression: + Progressive Symmetry Room: + panel_doors: + - NEAR FAR + - room: Warts Straw Area + panel_door: WARTS STRAW + - room: Leaf Feel Area + panel_door: LEAF FEEL Warts Straw Area: entrances: Near Far Area: @@ -1097,6 +1227,13 @@ panels: - WARTS - STRAW + panel_doors: + WARTS STRAW: + item_name: Symmetry Room - WARTS, STRAW (Panels) + panel_group: Symmetry Room Panels + panels: + - WARTS + - STRAW Leaf Feel Area: entrances: Warts Straw Area: @@ -1124,6 +1261,13 @@ panels: - LEAF - FEEL + panel_doors: + LEAF FEEL: + item_name: Symmetry Room - LEAF, FEEL (Panels) + panel_group: Symmetry Room Panels + panels: + - LEAF + - FEEL Outside The Agreeable: entrances: Crossroads: @@ -1232,6 +1376,20 @@ panels: - room: Color Hunt panel: PURPLE + panel_doors: + MASSACRED: + panel_group: Tenacious Entrance Panels + panels: + - MASSACRED + BLACK: + panels: + - BLACK + CLOSE: + panels: + - CLOSE + RIGHT: + panels: + - RIGHT paintings: - id: eyes_yellow_painting orientation: east @@ -1283,6 +1441,14 @@ - WINTER - DIAMONDS - FIRE + panel_doors: + Lookout: + item_name: Compass Room Panels + panels: + - NORTH + - WINTER + - DIAMONDS + - FIRE paintings: - id: pencil_painting7 orientation: north @@ -1392,6 +1558,8 @@ room: Owl Hallway door: Shortcut to Hedge Maze Roof: True + The Incomparable: + door: Observant Entrance panels: DOWN: id: Maze Room/Panel_down_up @@ -1499,6 +1667,10 @@ - HIDE (3) - room: Outside The Agreeable panel: HIDE + panel_doors: + DOWN: + panels: + - DOWN The Perceptive: entrances: Starting Room: @@ -1520,6 +1692,10 @@ check: True exclude_reduce: True tag: botwhite + panel_doors: + GAZE: + panels: + - GAZE paintings: - id: garden_painting_tower orientation: north @@ -1561,9 +1737,10 @@ - EAT progression: Progressive Fearless: - - Second Floor - - room: The Fearless (Second Floor) - door: Third Floor + doors: + - Second Floor + - room: The Fearless (Second Floor) + door: Third Floor The Fearless (Second Floor): entrances: The Fearless (First Floor): @@ -1658,6 +1835,10 @@ tag: forbid required_door: door: Stairs + required_panel: + - panel: FOUR (1) + - panel: FOUR (2) + - panel: SIX achievement: The Observant FOUR (1): id: Look Room/Panel_four_back @@ -1771,15 +1952,31 @@ door_group: Observant Doors panels: - SIX + panel_doors: + BACKSIDE: + item_name: The Observant - Backside Entrance Panels + panel_group: Backside Entrance Panels + panels: + - FOUR (1) + - FOUR (2) + STAIRS: + panels: + - SIX The Incomparable: entrances: The Observant: warp: True - Eight Room: True + Eight Room: + # It is possible to get to the second floor warpless, but there are no warpless exits from the second floor, + # meaning that this connection is essentially always a warp for the purposes of Pilgrimage. + warp: True Eight Alcove: door: Eight Door Orange Tower Sixth Floor: painting: True + Hedge Maze: + room: Hedge Maze + door: Observant Entrance panels: Achievement: id: Countdown Panels/Panel_incomparable_incomparable @@ -1787,9 +1984,12 @@ check: True tag: forbid required_room: - - Elements Area - - Courtyard - Eight Room + required_panel: + - room: Courtyard + panel: I + - room: Elements Area + panel: A achievement: The Incomparable A (One): id: Strand Room/Panel_blank_a @@ -1854,6 +2054,27 @@ panel: I - room: Elements Area panel: A + Eight Door (Outside The Initiated): + id: Red Blue Purple Room Area Doors/Door_a_strands2 + item_name: Outside The Initiated - Eight Door + item_group: Achievement Room Entrances + skip_location: True + panels: + - room: The Incomparable + panel: I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A + panel_doors: + Giant Sevens: + item_name: Giant Seven Panels + panels: + - I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A paintings: - id: crown_painting orientation: east @@ -1863,8 +2084,8 @@ room: The Incomparable door: Eight Door Outside The Initiated: - room: Outside The Initiated - door: Eight Door + room: The Incomparable + door: Eight Door (Outside The Initiated) paintings: - id: eight_painting2 orientation: north @@ -1961,14 +2182,31 @@ panel: DRAWL + RUNS - room: Owl Hallway panel: READS + RUST + panel_doors: + Access: + item_name: Orange Tower Panels + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST progression: Progressive Orange Tower: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor - - Sixth Floor - - Seventh Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Sixth Floor + - Seventh Floor Orange Tower First Floor: entrances: Hub Room: @@ -2011,6 +2249,10 @@ - SALT - room: Directional Gallery panel: PEPPER + panel_doors: + SECRET: + panels: + - SECRET sunwarps: - dots: 4 direction: enter @@ -2163,6 +2405,10 @@ id: Shuffle Room Area Doors/Door_hotcrust_shortcuts panels: - HOT CRUSTS + panel_doors: + HOT CRUSTS: + panels: + - HOT CRUSTS sunwarps: - dots: 5 direction: enter @@ -2277,6 +2523,12 @@ panels: - SIZE (Small) - SIZE (Big) + panel_doors: + SIZE: + item_name: Orange Tower Fifth Floor - SIZE Panels + panels: + - SIZE (Small) + - SIZE (Big) paintings: - id: hi_solved_painting3 orientation: south @@ -2313,7 +2565,7 @@ orientation: east - id: hi_solved_painting orientation: west - Orange Tower Seventh Floor: + Ending Area: entrances: Orange Tower Sixth Floor: room: Orange Tower @@ -2325,6 +2577,18 @@ check: True tag: forbid non_counting: True + location_name: Orange Tower Seventh Floor - THE END + doors: + End: + event: True + panels: + - THE END + Orange Tower Seventh Floor: + entrances: + Ending Area: + room: Ending Area + door: End + panels: THE MASTER: # We will set up special rules for this in code. id: Countdown Panels/Panel_master_master @@ -2608,6 +2872,15 @@ - SECOND - THIRD - FOURTH + panel_doors: + FIRST SECOND THIRD FOURTH: + item_name: Courtyard - Ordinal Panels + panel_group: Backside Entrance Panels + panels: + - FIRST + - SECOND + - THIRD + - FOURTH The Colorful (White): entrances: Courtyard: True @@ -2625,6 +2898,12 @@ location_name: The Colorful - White panels: - BEGIN + panel_doors: + BEGIN: + item_name: The Colorful - BEGIN (Panel) + panel_group: Colorful Panels + panels: + - BEGIN The Colorful (Black): entrances: The Colorful (White): @@ -2645,6 +2924,12 @@ door_group: Colorful Doors panels: - FOUND + panel_doors: + FOUND: + item_name: The Colorful - FOUND (Panel) + panel_group: Colorful Panels + panels: + - FOUND The Colorful (Red): entrances: The Colorful (Black): @@ -2665,6 +2950,12 @@ door_group: Colorful Doors panels: - LOAF + panel_doors: + LOAF: + item_name: The Colorful - LOAF (Panel) + panel_group: Colorful Panels + panels: + - LOAF The Colorful (Yellow): entrances: The Colorful (Red): @@ -2685,6 +2976,12 @@ door_group: Colorful Doors panels: - CREAM + panel_doors: + CREAM: + item_name: The Colorful - CREAM (Panel) + panel_group: Colorful Panels + panels: + - CREAM The Colorful (Blue): entrances: The Colorful (Yellow): @@ -2705,6 +3002,12 @@ door_group: Colorful Doors panels: - SUN + panel_doors: + SUN: + item_name: The Colorful - SUN (Panel) + panel_group: Colorful Panels + panels: + - SUN The Colorful (Purple): entrances: The Colorful (Blue): @@ -2725,6 +3028,12 @@ door_group: Colorful Doors panels: - SPOON + panel_doors: + SPOON: + item_name: The Colorful - SPOON (Panel) + panel_group: Colorful Panels + panels: + - SPOON The Colorful (Orange): entrances: The Colorful (Purple): @@ -2745,6 +3054,12 @@ door_group: Colorful Doors panels: - LETTERS + panel_doors: + LETTERS: + item_name: The Colorful - LETTERS (Panel) + panel_group: Colorful Panels + panels: + - LETTERS The Colorful (Green): entrances: The Colorful (Orange): @@ -2765,6 +3080,12 @@ door_group: Colorful Doors panels: - WALLS + panel_doors: + WALLS: + item_name: The Colorful - WALLS (Panel) + panel_group: Colorful Panels + panels: + - WALLS The Colorful (Brown): entrances: The Colorful (Green): @@ -2785,6 +3106,12 @@ door_group: Colorful Doors panels: - IRON + panel_doors: + IRON: + item_name: The Colorful - IRON (Panel) + panel_group: Colorful Panels + panels: + - IRON The Colorful (Gray): entrances: The Colorful (Brown): @@ -2805,6 +3132,12 @@ door_group: Colorful Doors panels: - OBSTACLE + panel_doors: + OBSTACLE: + item_name: The Colorful - OBSTACLE (Panel) + panel_group: Colorful Panels + panels: + - OBSTACLE The Colorful: entrances: The Colorful (Gray): @@ -2843,26 +3176,48 @@ orientation: north progression: Progressive Colorful: - - room: The Colorful (White) - door: Progress Door - - room: The Colorful (Black) - door: Progress Door - - room: The Colorful (Red) - door: Progress Door - - room: The Colorful (Yellow) - door: Progress Door - - room: The Colorful (Blue) - door: Progress Door - - room: The Colorful (Purple) - door: Progress Door - - room: The Colorful (Orange) - door: Progress Door - - room: The Colorful (Green) - door: Progress Door - - room: The Colorful (Brown) - door: Progress Door - - room: The Colorful (Gray) - door: Progress Door + doors: + - room: The Colorful (White) + door: Progress Door + - room: The Colorful (Black) + door: Progress Door + - room: The Colorful (Red) + door: Progress Door + - room: The Colorful (Yellow) + door: Progress Door + - room: The Colorful (Blue) + door: Progress Door + - room: The Colorful (Purple) + door: Progress Door + - room: The Colorful (Orange) + door: Progress Door + - room: The Colorful (Green) + door: Progress Door + - room: The Colorful (Brown) + door: Progress Door + - room: The Colorful (Gray) + door: Progress Door + panel_doors: + - room: The Colorful (White) + panel_door: BEGIN + - room: The Colorful (Black) + panel_door: FOUND + - room: The Colorful (Red) + panel_door: LOAF + - room: The Colorful (Yellow) + panel_door: CREAM + - room: The Colorful (Blue) + panel_door: SUN + - room: The Colorful (Purple) + panel_door: SPOON + - room: The Colorful (Orange) + panel_door: LETTERS + - room: The Colorful (Green) + panel_door: WALLS + - room: The Colorful (Brown) + panel_door: IRON + - room: The Colorful (Gray) + panel_door: OBSTACLE Welcome Back Area: entrances: Starting Room: @@ -2935,6 +3290,10 @@ door_group: Hedge Maze Doors panels: - STRAYS + panel_doors: + STRAYS: + panels: + - STRAYS paintings: - id: arrows_painting_8 orientation: south @@ -2968,7 +3327,8 @@ room: Art Gallery door: Exit Eight Alcove: - door: Eight Door + room: The Incomparable + door: Eight Door (Outside The Initiated) The Optimistic: True panels: SEVEN (1): @@ -3121,17 +3481,13 @@ panel: GREEN - room: Outside The Agreeable panel: PURPLE - Eight Door: - id: Red Blue Purple Room Area Doors/Door_a_strands2 - item_group: Achievement Room Entrances - skip_location: True + panel_doors: + UNCOVER: panels: - - room: The Incomparable - panel: I (Seven) - - room: Courtyard - panel: I - - room: Elements Area - panel: A + - UNCOVER + OXEN: + panels: + - OXEN paintings: - id: clock_painting_5 orientation: east @@ -3265,7 +3621,6 @@ door: Traveled Entrance Color Hallways: door: Color Hallways Entrance - warp: True panels: Achievement: id: Countdown Panels/Panel_traveled_traveled @@ -3502,6 +3857,13 @@ - RISE (Sunrise) - ZEN - SON + panel_doors: + UNOPEN: + panels: + - UNOPEN + BEGIN: + panels: + - BEGIN paintings: - id: pencil_painting2 orientation: west @@ -3797,6 +4159,34 @@ item_group: Achievement Room Entrances panels: - ZERO + panel_doors: + ZERO: + panels: + - ZERO + PEN: + panels: + - PEN + TWO: + item_name: Two Panels + panels: + - TWO (1) + - TWO (2) + THREE: + item_name: Three Panels + panels: + - THREE (1) + - THREE (2) + - THREE (3) + FOUR: + item_name: Four Panels + panels: + - FOUR + - room: Hub Room + panel: FOUR + - room: Dead End Area + panel: FOUR + - room: The Traveled + panel: FOUR paintings: - id: maze_painting_3 enter_only: True @@ -3972,6 +4362,10 @@ panel: FIVE (1) - room: Directional Gallery panel: FIVE (2) + First Six: + event: True + panels: + - SIX Sevens: id: - Count Up Room Area Doors/Door_seven_hider @@ -4080,12 +4474,109 @@ panel: NINE - room: Elements Area panel: NINE + panel_doors: + FIVE: + item_name: Five Panels + panels: + - FIVE + - room: Outside The Agreeable + panel: FIVE (1) + - room: Outside The Agreeable + panel: FIVE (2) + - room: Directional Gallery + panel: FIVE (1) + - room: Directional Gallery + panel: FIVE (2) + SIX: + item_name: Six Panels + panels: + - SIX + - room: Outside The Bold + panel: SIX + - room: Directional Gallery + panel: SIX (1) + - room: Directional Gallery + panel: SIX (2) + - room: The Bearer (East) + panel: SIX + - room: The Bearer (South) + panel: SIX + SEVEN: + item_name: Seven Panels + panels: + - SEVEN + - room: Directional Gallery + panel: SEVEN + - room: Knight Night Exit + panel: SEVEN (1) + - room: Knight Night Exit + panel: SEVEN (2) + - room: Knight Night Exit + panel: SEVEN (3) + - room: Outside The Initiated + panel: SEVEN (1) + - room: Outside The Initiated + panel: SEVEN (2) + EIGHT: + item_name: Eight Panels + panels: + - EIGHT + - room: Directional Gallery + panel: EIGHT + - room: The Eyes They See + panel: EIGHT + - room: Dead End Area + panel: EIGHT + - room: Crossroads + panel: EIGHT + - room: Hot Crusts Area + panel: EIGHT + - room: Art Gallery + panel: EIGHT + - room: Outside The Initiated + panel: EIGHT + NINE: + item_name: Nine Panels + panels: + - NINE + - room: Directional Gallery + panel: NINE + - room: Amen Name Area + panel: NINE + - room: Yellow Backside Area + panel: NINE + - room: Outside The Initiated + panel: NINE + - room: Outside The Bold + panel: NINE + - room: Rhyme Room (Cross) + panel: NINE + - room: Orange Tower Fifth Floor + panel: NINE + - room: Elements Area + panel: NINE paintings: - id: smile_painting_5 enter_only: True orientation: east required_door: door: Eights + progression: + Progressive Number Hunt: + panel_doors: + - room: Outside The Undeterred + panel_door: TWO + - room: Outside The Undeterred + panel_door: THREE + - room: Outside The Undeterred + panel_door: FOUR + - FIVE + - SIX + - SEVEN + - EIGHT + - NINE + - room: Outside The Undeterred + panel_door: ZERO Directional Gallery: entrances: Outside The Agreeable: @@ -4173,7 +4664,7 @@ tag: midorange required_door: room: Number Hunt - door: Sixes + door: First Six PARANOID: id: Backside Room/Panel_paranoid_paranoid tag: midwhite @@ -4181,7 +4672,7 @@ exclude_reduce: True required_door: room: Number Hunt - door: Sixes + door: First Six YELLOW: id: Color Arrow Room/Panel_yellow_afar tag: midwhite @@ -4244,6 +4735,11 @@ panels: - room: Color Hunt panel: YELLOW + panel_doors: + TURN LEARN: + panels: + - TURN + - LEARN paintings: - id: smile_painting_7 orientation: south @@ -4255,7 +4751,7 @@ move: True required_door: room: Number Hunt - door: Sixes + door: First Six - id: boxes_painting orientation: south - id: cherry_painting @@ -4322,6 +4818,34 @@ id: Rock Room Doors/Door_hint panels: - EXIT + panel_doors: + EXIT: + panels: + - EXIT + RED: + panel_group: Color Hunt Panels + panels: + - RED + BLUE: + panel_group: Color Hunt Panels + panels: + - BLUE + YELLOW: + panel_group: Color Hunt Panels + panels: + - YELLOW + ORANGE: + panel_group: Color Hunt Panels + panels: + - ORANGE + PURPLE: + panel_group: Color Hunt Panels + panels: + - PURPLE + GREEN: + panel_group: Color Hunt Panels + panels: + - GREEN paintings: - id: arrows_painting_7 orientation: east @@ -4459,6 +4983,14 @@ event: True panels: - HEART + panel_doors: + FARTHER: + panel_group: Backside Entrance Panels + panels: + - FARTHER + MIDDLE: + panels: + - MIDDLE The Bearer (East): entrances: Cross Tower (East): True @@ -5311,6 +5843,11 @@ item_name: Knight Night Room - Exit panels: - TRUSTED + panel_doors: + TRUSTED: + item_name: Knight Night Room - TRUSTED (Panel) + panels: + - TRUSTED Knight Night Exit: entrances: Knight Night (Outer Ring): @@ -5995,6 +6532,10 @@ item_group: Achievement Room Entrances panels: - SHRINK + panel_doors: + SHRINK: + panels: + - SHRINK The Wondrous (Doorknob): entrances: Outside The Wondrous: @@ -6206,18 +6747,36 @@ - KEEP - BAILEY - TOWER + panel_doors: + CASTLE: + item_name: Hallway Room - First Room Panels + panel_group: Hallway Room Panels + panels: + - WALL + - KEEP + - BAILEY + - TOWER paintings: - id: panda_painting orientation: south progression: Progressive Hallway Room: - - Exit - - room: Hallway Room (2) - door: Exit - - room: Hallway Room (3) - door: Exit - - room: Hallway Room (4) - door: Exit + doors: + - Exit + - room: Hallway Room (2) + door: Exit + - room: Hallway Room (3) + door: Exit + - room: Hallway Room (4) + door: Exit + panel_doors: + - CASTLE + - room: Hallway Room (2) + panel_door: COUNTERCLOCKWISE + - room: Hallway Room (3) + panel_door: TRANSFORMATION + - room: Hallway Room (4) + panel_door: WHEELBARROW Hallway Room (2): entrances: Hallway Room (1): @@ -6256,6 +6815,15 @@ - CLOCK - ER - COUNT + panel_doors: + COUNTERCLOCKWISE: + item_name: Hallway Room - Second Room Panels + panel_group: Hallway Room Panels + panels: + - WISE + - CLOCK + - ER + - COUNT Hallway Room (3): entrances: Hallway Room (2): @@ -6294,6 +6862,15 @@ - FORM - A - SHUN + panel_doors: + TRANSFORMATION: + item_name: Hallway Room - Third Room Panels + panel_group: Hallway Room Panels + panels: + - TRANCE + - FORM + - A + - SHUN Hallway Room (4): entrances: Hallway Room (3): @@ -6316,6 +6893,12 @@ panels: - WHEEL include_reduce: True + panel_doors: + WHEELBARROW: + item_name: Hallway Room - WHEEL + panel_group: Hallway Room Panels + panels: + - WHEEL Elements Area: entrances: Roof: True @@ -6390,6 +6973,10 @@ panels: - room: The Wanderer panel: Achievement + panel_doors: + WANDERLUST: + panels: + - WANDERLUST The Wanderer: entrances: Outside The Wanderer: @@ -6531,6 +7118,10 @@ item_group: Achievement Room Entrances panels: - ORDER + panel_doors: + ORDER: + panels: + - ORDER paintings: - id: smile_painting_3 orientation: west @@ -6544,10 +7135,11 @@ orientation: south progression: Progressive Art Gallery: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor Art Gallery (Second Floor): entrances: Art Gallery: @@ -7069,6 +7661,8 @@ LEAP: id: Double Room/Panel_leap_leap tag: midwhite + required_door: + door: Door to Cross doors: Door to Cross: id: Double Room Area Doors/Door_room_4a @@ -7259,8 +7853,8 @@ id: Panel Room/Panel_broomed_bedroom colors: yellow tag: midyellow - required_door: - door: Excavation + required_panel: + panel: WALL (1) LAYS: id: Panel Room/Panel_lays_maze colors: purple @@ -7287,13 +7881,24 @@ Excavation: event: True panels: - - WALL (1) + - STAIRS Cellar Exit: id: - Tower Room Area Doors/Door_panel_basement - Tower Room Area Doors/Door_panel_basement2 panels: - BASE + panel_doors: + STAIRS: + panel_group: Room Room Panels + panels: + - STAIRS + Colors: + panel_group: Room Room Panels + panels: + - BROOMED + - LAYS + - BASE Cellar: entrances: Room Room: @@ -7332,6 +7937,11 @@ panels: - KITTEN - CAT + panel_doors: + KITTEN CAT: + panels: + - KITTEN + - CAT paintings: - id: arrows_painting_2 orientation: east @@ -7586,6 +8196,10 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + OPEN: + panels: + - OPEN The Scientific: entrances: Outside The Scientific: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 6c8c925138aa..8b159d4ae4ac 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 1fa06d24254f..13b77145ea2c 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -272,8 +272,9 @@ panels: PAINTING (4): 445081 PAINTING (5): 445082 ROOM: 445083 - Orange Tower Seventh Floor: + Ending Area: THE END: 444620 + Orange Tower Seventh Floor: THE MASTER: 444621 MASTERY: 444622 Behind A Smile: @@ -1123,6 +1124,8 @@ doors: Eight Door: item: 444475 location: 445219 + Eight Door (Outside The Initiated): + item: 444578 Orange Tower: Second Floor: item: 444476 @@ -1241,8 +1244,6 @@ doors: Entrance: item: 444516 location: 445237 - Eight Door: - item: 444578 The Traveled: Color Hallways Entrance: item: 444517 @@ -1477,3 +1478,145 @@ progression: Progressive Art Gallery: 444563 Progressive Colorful: 444580 Progressive Pilgrimage: 444583 + Progressive Suits Area: 444602 + Progressive Symmetry Room: 444608 + Progressive Number Hunt: 444654 +panel_doors: + Starting Room: + HIDDEN: 444589 + Hidden Room: + OPEN: 444590 + Hub Room: + ORDER: 444591 + SLAUGHTER: 444592 + TRACE: 444594 + RAT: 444595 + OPEN: 444596 + Crossroads: + DECAY: 444597 + NOPE: 444598 + WE ROT: 444599 + WORDS SWORD: 444600 + BEND HI: 444601 + Lost Area: + LOST: 444603 + Amen Name Area: + AMEN NAME: 444604 + The Tenacious: + Black Palindromes: 444605 + Near Far Area: + NEAR FAR: 444606 + Warts Straw Area: + WARTS STRAW: 444609 + Leaf Feel Area: + LEAF FEEL: 444610 + Outside The Agreeable: + MASSACRED: 444611 + BLACK: 444612 + CLOSE: 444613 + RIGHT: 444614 + Compass Room: + Lookout: 444615 + Hedge Maze: + DOWN: 444617 + The Perceptive: + GAZE: 444618 + The Observant: + BACKSIDE: 444619 + STAIRS: 444621 + The Incomparable: + Giant Sevens: 444622 + Orange Tower: + Access: 444623 + Orange Tower First Floor: + SECRET: 444624 + Orange Tower Fourth Floor: + HOT CRUSTS: 444625 + Orange Tower Fifth Floor: + SIZE: 444626 + First Second Third Fourth: + FIRST SECOND THIRD FOURTH: 444627 + The Colorful (White): + BEGIN: 444628 + The Colorful (Black): + FOUND: 444630 + The Colorful (Red): + LOAF: 444631 + The Colorful (Yellow): + CREAM: 444632 + The Colorful (Blue): + SUN: 444633 + The Colorful (Purple): + SPOON: 444634 + The Colorful (Orange): + LETTERS: 444635 + The Colorful (Green): + WALLS: 444636 + The Colorful (Brown): + IRON: 444637 + The Colorful (Gray): + OBSTACLE: 444638 + Owl Hallway: + STRAYS: 444639 + Outside The Initiated: + UNCOVER: 444640 + OXEN: 444641 + Outside The Bold: + UNOPEN: 444642 + BEGIN: 444643 + Outside The Undeterred: + ZERO: 444644 + PEN: 444645 + TWO: 444646 + THREE: 444647 + FOUR: 444648 + Number Hunt: + FIVE: 444649 + SIX: 444650 + SEVEN: 444651 + EIGHT: 444652 + NINE: 444653 + Color Hunt: + EXIT: 444655 + RED: 444656 + BLUE: 444658 + YELLOW: 444659 + ORANGE: 444660 + PURPLE: 444661 + GREEN: 444662 + The Bearer: + FARTHER: 444663 + MIDDLE: 444664 + Knight Night (Final): + TRUSTED: 444665 + Outside The Wondrous: + SHRINK: 444666 + Hallway Room (1): + CASTLE: 444667 + Hallway Room (2): + COUNTERCLOCKWISE: 444669 + Hallway Room (3): + TRANSFORMATION: 444670 + Hallway Room (4): + WHEELBARROW: 444671 + Outside The Wanderer: + WANDERLUST: 444672 + Art Gallery: + ORDER: 444673 + Room Room: + STAIRS: 444674 + Colors: 444676 + Outside The Wise: + KITTEN CAT: 444677 + Outside The Scientific: + OPEN: 444678 + Directional Gallery: + TURN LEARN: 444679 +panel_groups: + Tenacious Entrance Panels: 444593 + Symmetry Room Panels: 444607 + Backside Entrance Panels: 444620 + Colorful Panels: 444629 + Color Hunt Panels: 444657 + Hallway Room Panels: 444668 + Room Room Panels: 444675 diff --git a/worlds/lingo/datatypes.py b/worlds/lingo/datatypes.py index 36141daa4106..9521422ab154 100644 --- a/worlds/lingo/datatypes.py +++ b/worlds/lingo/datatypes.py @@ -12,6 +12,11 @@ class RoomAndPanel(NamedTuple): panel: str +class RoomAndPanelDoor(NamedTuple): + room: Optional[str] + panel_door: str + + class EntranceType(Flag): NORMAL = auto() PAINTING = auto() @@ -63,9 +68,15 @@ class Panel(NamedTuple): exclude_reduce: bool achievement: bool non_counting: bool + panel_door: Optional[RoomAndPanelDoor] # This will always be fully specified. location_name: Optional[str] +class PanelDoor(NamedTuple): + item_name: str + panel_group: Optional[str] + + class Painting(NamedTuple): id: str room: str diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py index 67eaceab10fe..78b288e7c2df 100644 --- a/worlds/lingo/items.py +++ b/worlds/lingo/items.py @@ -3,7 +3,7 @@ from BaseClasses import Item, ItemClassification from .static_logic import DOORS_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, get_door_item_id, \ - get_progressive_item_id, get_special_item_id + get_progressive_item_id, get_special_item_id, PANEL_DOORS_BY_ROOM, get_panel_door_item_id, get_panel_group_item_id class ItemType(Enum): @@ -65,6 +65,21 @@ def load_item_data(): ItemClassification.progression, ItemType.NORMAL, True, []) ITEMS_BY_GROUP.setdefault("Doors", []).append(group) + panel_groups: Set[str] = set() + for room_name, panel_doors in PANEL_DOORS_BY_ROOM.items(): + for panel_door_name, panel_door in panel_doors.items(): + if panel_door.panel_group is not None: + panel_groups.add(panel_door.panel_group) + + ALL_ITEM_TABLE[panel_door.item_name] = ItemData(get_panel_door_item_id(room_name, panel_door_name), + ItemClassification.progression, ItemType.NORMAL, False, []) + ITEMS_BY_GROUP.setdefault("Panels", []).append(panel_door.item_name) + + for group in panel_groups: + ALL_ITEM_TABLE[group] = ItemData(get_panel_group_item_id(group), ItemClassification.progression, + ItemType.NORMAL, False, []) + ITEMS_BY_GROUP.setdefault("Panels", []).append(group) + special_items: Dict[str, ItemClassification] = { ":)": ItemClassification.filler, "The Feeling of Being Lost": ItemClassification.filler, diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 333b3e1ef08e..2d6e9967dfc4 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -8,21 +8,31 @@ class ShuffleDoors(Choice): - """If on, opening doors will require their respective "keys". + """This option specifies how doors open. - - **Simple:** Doors are sorted into logical groups, which are all opened by - receiving an item. - - **Complex:** The items are much more granular, and will usually only open - a single door each. + - **None:** Doors in the game will open the way they do in vanilla. + - **Panels:** Doors still open as in vanilla, but the panels that open the + doors will be locked, and an item will be required to unlock the panels. + - **Doors:** the doors themselves are locked behind items, and will open + automatically without needing to solve a panel once the key is obtained. """ display_name = "Shuffle Doors" option_none = 0 - option_simple = 1 - option_complex = 2 + option_panels = 1 + option_doors = 2 + alias_simple = 2 + alias_complex = 2 + + +class GroupDoors(Toggle): + """By default, door shuffle in either panels or doors mode will create individual keys for every panel or door to be locked. + + When group doors is on, some panels and doors are sorted into logical groups, which are opened together by receiving an item.""" + display_name = "Group Doors" class ProgressiveOrangeTower(DefaultOnToggle): - """When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up. + """When "Shuffle Doors" is on doors mode, this setting governs the manner in which the Orange Tower floors open up. - **Off:** There is an item for each floor of the tower, and each floor's item is the only one needed to access that floor. @@ -33,7 +43,7 @@ class ProgressiveOrangeTower(DefaultOnToggle): class ProgressiveColorful(DefaultOnToggle): - """When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up. + """When "Shuffle Doors" is on either panels or doors mode and "Group Doors" is off, this setting governs the manner in which The Colorful opens up. - **Off:** There is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you @@ -70,10 +80,15 @@ class ShuffleColors(DefaultOnToggle): class ShufflePanels(Choice): - """If on, the puzzles on each panel are randomized. + """Determines how panel puzzles are randomized. + + - **None:** Most panels remain the same as in the base game. Note that there are + some panels (in particular, in Starting Room and Second Room) that are changed + by the randomizer even when panel shuffle is disabled. + - **Rearrange:** The puzzles are the same as the ones in the base game, but are + placed in different areas. - On "rearrange", the puzzles are the same as the ones in the base game, but - are placed in different areas. + More options for puzzle randomization are planned in the future. """ display_name = "Shuffle Panels" option_none = 0 @@ -194,6 +209,11 @@ class EarlyColorHallways(Toggle): display_name = "Early Color Hallways" +class ShufflePostgame(Toggle): + """When off, locations that could not be reached without also reaching your victory condition are removed.""" + display_name = "Shuffle Postgame" + + class TrapPercentage(Range): """Replaces junk items with traps, at the specified rate.""" display_name = "Trap Percentage" @@ -248,6 +268,7 @@ class DeathLink(Toggle): @dataclass class LingoOptions(PerGameCommonOptions): shuffle_doors: ShuffleDoors + group_doors: GroupDoors progressive_orange_tower: ProgressiveOrangeTower progressive_colorful: ProgressiveColorful location_checks: LocationChecks @@ -263,6 +284,7 @@ class LingoOptions(PerGameCommonOptions): mastery_achievements: MasteryAchievements level_2_requirement: Level2Requirement early_color_hallways: EarlyColorHallways + shuffle_postgame: ShufflePostgame trap_percentage: TrapPercentage trap_weights: TrapWeights puzzle_skip_percentage: PuzzleSkipPercentage diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 1621620e1e14..83217d7311a3 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -7,8 +7,8 @@ from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, SunwarpAccess, VictoryCondition from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \ - PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, \ - SUNWARP_ENTRANCES, SUNWARP_EXITS + PANELS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, PROGRESSIVE_DOORS_BY_ROOM, \ + PANEL_DOORS_BY_ROOM, PROGRESSIVE_PANELS_BY_ROOM, SUNWARP_ENTRANCES, SUNWARP_EXITS if TYPE_CHECKING: from . import LingoWorld @@ -18,23 +18,35 @@ class AccessRequirements: rooms: Set[str] doors: Set[RoomAndDoor] colors: Set[str] + items: Set[str] + progression: Dict[str, int] the_master: bool + postgame: bool def __init__(self): self.rooms = set() self.doors = set() self.colors = set() + self.items = set() + self.progression = dict() self.the_master = False + self.postgame = False def merge(self, other: "AccessRequirements"): self.rooms |= other.rooms self.doors |= other.doors self.colors |= other.colors + self.items |= other.items self.the_master |= other.the_master + self.postgame |= other.postgame + + for progression, index in other.progression.items(): + if progression not in self.progression or index > self.progression[progression]: + self.progression[progression] = index def __str__(self): - return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \ - f" the_master={self.the_master}" + return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}, items={self.items}," \ + f" progression={self.progression}), the_master={self.the_master}, postgame={self.postgame}" class PlayerLocation(NamedTuple): @@ -114,15 +126,15 @@ def set_door_item(self, room: str, door: str, item: str): self.item_by_door.setdefault(room, {})[door] = item def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"): - if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]: - progression_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name + if room_name in PROGRESSIVE_DOORS_BY_ROOM and door_data.name in PROGRESSIVE_DOORS_BY_ROOM[room_name]: + progression_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name progression_handling = should_split_progression(progression_name, world) if progression_handling == ProgressiveItemBehavior.SPLIT: self.set_door_item(room_name, door_data.name, door_data.item_name) self.real_items.append(door_data.item_name) elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE: - progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name + progressive_item_name = PROGRESSIVE_DOORS_BY_ROOM[room_name][door_data.name].item_name self.set_door_item(room_name, door_data.name, progressive_item_name) self.real_items.append(progressive_item_name) else: @@ -153,17 +165,31 @@ def __init__(self, world: "LingoWorld"): victory_condition = world.options.victory_condition early_color_hallways = world.options.early_color_hallways - if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none: - raise OptionError("You cannot have reduced location checks when door shuffle is on, because there would not" - " be enough locations for all of the door items.") + if location_checks == LocationChecks.option_reduced: + if door_shuffle == ShuffleDoors.option_doors: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when door shuffle" + f" is on, because there would not be enough locations for all of the door items.") + if door_shuffle == ShuffleDoors.option_panels: + if not world.options.group_doors: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks when ungrouped" + f" panels mode door shuffle is on, because there would not be enough locations for" + f" all of the panel items.") + if color_shuffle: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both" + f" panels mode door shuffle and color shuffle because there would not be enough" + f" locations for all of the items.") + if world.options.sunwarp_access >= SunwarpAccess.option_individual: + raise OptionError(f"Slot \"{world.player_name}\" cannot have reduced location checks with both" + f" panels mode door shuffle and individual or progressive sunwarp access because" + f" there would not be enough locations for all of the items.") # Create door items, where needed. door_groups: Set[str] = set() for room_name, room_data in DOORS_BY_ROOM.items(): for door_name, door_data in room_data.items(): if door_data.skip_item is False and door_data.event is False: - if door_data.type == DoorType.NORMAL and door_shuffle != ShuffleDoors.option_none: - if door_data.door_group is not None and door_shuffle == ShuffleDoors.option_simple: + if door_data.type == DoorType.NORMAL and door_shuffle == ShuffleDoors.option_doors: + if door_data.door_group is not None and world.options.group_doors: # Grouped doors are handled differently if shuffle doors is on simple. self.set_door_item(room_name, door_name, door_data.door_group) door_groups.add(door_data.door_group) @@ -185,21 +211,33 @@ def __init__(self, world: "LingoWorld"): self.real_items.append(door_data.item_name) self.real_items += door_groups - + + # Create panel items, where needed. + if world.options.shuffle_doors == ShuffleDoors.option_panels: + panel_groups: Set[str] = set() + + for room_name, room_data in PANEL_DOORS_BY_ROOM.items(): + for panel_door_name, panel_door_data in room_data.items(): + if panel_door_data.panel_group is not None and world.options.group_doors: + panel_groups.add(panel_door_data.panel_group) + elif room_name in PROGRESSIVE_PANELS_BY_ROOM \ + and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[room_name]: + progression_obj = PROGRESSIVE_PANELS_BY_ROOM[room_name][panel_door_name] + progression_handling = should_split_progression(progression_obj.item_name, world) + + if progression_handling == ProgressiveItemBehavior.SPLIT: + self.real_items.append(panel_door_data.item_name) + elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE: + self.real_items.append(progression_obj.item_name) + else: + self.real_items.append(panel_door_data.item_name) + + self.real_items += panel_groups + # Create color items, if needed. if color_shuffle: self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR] - # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. - for room_name, room_data in PANELS_BY_ROOM.items(): - for panel_name, panel_data in room_data.items(): - if panel_data.achievement: - access_req = AccessRequirements() - access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) - access_req.rooms.add(room_name) - - self.mastery_reqs.append(access_req) - # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need # to prevent the actual victory condition from becoming a check. self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" @@ -207,7 +245,7 @@ def __init__(self, world: "LingoWorld"): if victory_condition == VictoryCondition.option_the_end: self.victory_condition = "Orange Tower Seventh Floor - THE END" - self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) + self.add_location("Ending Area", "The End (Solved)", None, [], world) self.event_loc_to_item["The End (Solved)"] = "Victory" elif victory_condition == VictoryCondition.option_the_master: self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" @@ -231,6 +269,16 @@ def __init__(self, world: "LingoWorld"): [RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world) self.event_loc_to_item["PILGRIM (Solved)"] = "Victory" + # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. + for room_name, room_data in PANELS_BY_ROOM.items(): + for panel_name, panel_data in room_data.items(): + if panel_data.achievement: + access_req = AccessRequirements() + access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) + access_req.rooms.add(room_name) + + self.mastery_reqs.append(access_req) + # Create groups of counting panel access requirements for the LEVEL 2 check. self.create_panel_hunt_events(world) @@ -241,7 +289,7 @@ def __init__(self, world: "LingoWorld"): elif location_checks == LocationChecks.option_insanity: location_classification = LocationClassification.insanity - if door_shuffle != ShuffleDoors.option_none and not early_color_hallways: + if door_shuffle == ShuffleDoors.option_doors and not early_color_hallways: location_classification |= LocationClassification.small_sphere_one for location_name, location_data in ALL_LOCATION_TABLE.items(): @@ -283,7 +331,7 @@ def __init__(self, world: "LingoWorld"): "iterations. This is very unlikely to happen on its own, and probably indicates some " "kind of logic error.") - if door_shuffle != ShuffleDoors.option_none and location_checks != LocationChecks.option_insanity \ + if door_shuffle == ShuffleDoors.option_doors and location_checks != LocationChecks.option_insanity \ and not early_color_hallways and world.multiworld.players > 1: # Under the combination of door shuffle, normal location checks, and no early color hallways, sphere 1 is # only three checks. In a multiplayer situation, this can be frustrating for the player because they are @@ -298,19 +346,19 @@ def __init__(self, world: "LingoWorld"): # Starting Room - Exit Door gives access to OPEN and TRACE. good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"] - if not color_shuffle and not world.options.enable_pilgrimage: - # HOT CRUST and THIS. - good_item_options.append("Pilgrim Room - Sun Painting") - if not color_shuffle: - if door_shuffle == ShuffleDoors.option_simple: + if not world.options.enable_pilgrimage: + # HOT CRUST and THIS. + good_item_options.append("Pilgrim Room - Sun Painting") + + if world.options.group_doors: # WELCOME BACK, CLOCKWISE, and DRAWL + RUNS. good_item_options.append("Welcome Back Doors") else: # WELCOME BACK and CLOCKWISE. good_item_options.append("Welcome Back Area - Shortcut to Starting Room") - if door_shuffle == ShuffleDoors.option_simple: + if world.options.group_doors: # Color hallways access (NOTE: reconsider when sunwarp shuffling exists). good_item_options.append("Rhyme Room Doors") @@ -356,17 +404,15 @@ def __init__(self, world: "LingoWorld"): def randomize_paintings(self, world: "LingoWorld") -> bool: self.painting_mapping.clear() - door_shuffle = world.options.shuffle_doors - # First, assign mappings to the required-exit paintings. We ensure that req-blocked paintings do not lead to # required paintings. req_exits = [] required_painting_rooms = REQUIRED_PAINTING_ROOMS - if door_shuffle == ShuffleDoors.option_none: + if world.options.shuffle_doors != ShuffleDoors.option_doors: required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS req_exits = [painting_id for painting_id, painting in PAINTINGS.items() if painting.required_when_no_doors] - def is_req_enterable(painting_id: str, painting: Painting) -> bool: + def is_req_enterable(painting: Painting) -> bool: if painting.exit_only or painting.disable or painting.req_blocked\ or painting.room in required_painting_rooms: return False @@ -387,7 +433,7 @@ def is_req_enterable(painting_id: str, painting: Painting) -> bool: return True req_enterable = [painting_id for painting_id, painting in PAINTINGS.items() - if is_req_enterable(painting_id, painting)] + if is_req_enterable(painting)] req_exits += [painting_id for painting_id, painting in PAINTINGS.items() if painting.exit_only and painting.required] req_entrances = world.random.sample(req_enterable, len(req_exits)) @@ -429,7 +475,7 @@ def is_req_enterable(painting_id: str, painting: Painting) -> bool: for painting_id, painting in PAINTINGS.items(): if painting_id not in self.painting_mapping.values() \ and (painting.required or (painting.required_when_no_doors and - door_shuffle == ShuffleDoors.option_none)): + world.options.shuffle_doors != ShuffleDoors.option_doors)): return False return True @@ -444,12 +490,31 @@ def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld access_reqs = AccessRequirements() panel_object = PANELS_BY_ROOM[room][panel] + if world.options.shuffle_doors == ShuffleDoors.option_panels and panel_object.panel_door is not None: + panel_door_room = panel_object.panel_door.room + panel_door_name = panel_object.panel_door.panel_door + panel_door = PANEL_DOORS_BY_ROOM[panel_door_room][panel_door_name] + + if panel_door.panel_group is not None and world.options.group_doors: + access_reqs.items.add(panel_door.panel_group) + elif panel_door_room in PROGRESSIVE_PANELS_BY_ROOM\ + and panel_door_name in PROGRESSIVE_PANELS_BY_ROOM[panel_door_room]: + progression_obj = PROGRESSIVE_PANELS_BY_ROOM[panel_door_room][panel_door_name] + progression_handling = should_split_progression(progression_obj.item_name, world) + + if progression_handling == ProgressiveItemBehavior.SPLIT: + access_reqs.items.add(panel_door.item_name) + elif progression_handling == ProgressiveItemBehavior.PROGRESSIVE: + access_reqs.progression[progression_obj.item_name] = progression_obj.index + else: + access_reqs.items.add(panel_door.item_name) + for req_room in panel_object.required_rooms: access_reqs.rooms.add(req_room) for req_door in panel_object.required_doors: door_object = DOORS_BY_ROOM[room if req_door.room is None else req_door.room][req_door.door] - if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none: + if door_object.event or world.options.shuffle_doors != ShuffleDoors.option_doors: sub_access_reqs = self.calculate_door_requirements( room if req_door.room is None else req_door.room, req_door.door, world) access_reqs.merge(sub_access_reqs) @@ -470,6 +535,11 @@ def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld if panel == "THE MASTER": access_reqs.the_master = True + # Evil python magic (so sayeth NewSoupVi): this checks victory_condition against the panel's location name + # override if it exists, or the auto-generated location name if it's None. + if self.victory_condition == (panel_object.location_name or f"{room} - {panel}"): + access_reqs.postgame = True + self.panel_reqs[room][panel] = access_reqs return self.panel_reqs[room][panel] @@ -514,11 +584,14 @@ def create_panel_hunt_events(self, world: "LingoWorld"): continue # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will - # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. THE MASTER has - # special access rules and is handled separately. + # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. Panel door locked + # puzzles will be separate if panels mode is on. THE MASTER has special access rules and is handled + # separately. if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ or len(panel_data.required_rooms) > 0\ or (world.options.shuffle_colors and len(panel_data.colors) > 1)\ + or (world.options.shuffle_doors == ShuffleDoors.option_panels + and panel_data.panel_door is not None)\ or panel_name == "THE MASTER": self.counting_panel_reqs.setdefault(room_name, []).append( (self.calculate_panel_requirements(room_name, panel_name, world), 1)) diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 9834f04f9de7..9773f22d9196 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -159,7 +159,7 @@ def create_regions(world: "LingoWorld") -> None: RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world) if early_color_hallways: - connect_entrance(regions, regions["Starting Room"], regions["Outside The Undeterred"], "Early Color Hallways", + connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways", None, EntranceType.PAINTING, False, world) if painting_shuffle: diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index d91c53f05b47..e0bb08fa1f7a 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -3,7 +3,7 @@ from BaseClasses import CollectionState from .datatypes import RoomAndDoor from .player_logic import AccessRequirements, PlayerLocation -from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS +from .static_logic import PROGRESSIVE_DOORS_BY_ROOM, PROGRESSIVE_ITEMS if TYPE_CHECKING: from . import LingoWorld @@ -59,9 +59,18 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir if not state.has(color.capitalize(), world.player): return False + if not all(state.has(item, world.player) for item in access.items): + return False + + if not all(state.has(item, world.player, index) for item, index in access.progression.items()): + return False + if access.the_master and not lingo_can_use_mastery_location(state, world): return False + if access.postgame and state.has("Prevent Victory", world.player): + return False + return True @@ -74,7 +83,7 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L item_name = world.player_logic.item_by_door[room][door] if item_name in PROGRESSIVE_ITEMS: - progression = PROGRESSION_BY_ROOM[room][door] + progression = PROGRESSIVE_DOORS_BY_ROOM[room][door] return state.has(item_name, world.player, progression.index) return state.has(item_name, world.player) diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index ff820dd0cb11..9925e9582a2c 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -4,15 +4,17 @@ from io import BytesIO from typing import Dict, List, Set -from .datatypes import Door, Painting, Panel, Progression, Room +from .datatypes import Door, Painting, Panel, PanelDoor, Progression, Room ALL_ROOMS: List[Room] = [] DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {} PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {} +PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {} PAINTINGS: Dict[str, Painting] = {} -PROGRESSIVE_ITEMS: List[str] = [] -PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_ITEMS: Set[str] = set() +PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} PAINTING_ENTRANCES: int = 0 PAINTING_EXIT_ROOMS: Set[str] = set() @@ -28,6 +30,8 @@ DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} DOOR_GROUP_ITEM_IDS: Dict[str, int] = {} +PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} +PANEL_GROUP_ITEM_IDS: Dict[str, int] = {} PROGRESSIVE_ITEM_IDS: Dict[str, int] = {} HASHES: Dict[str, str] = {} @@ -68,6 +72,20 @@ def get_door_group_item_id(name: str): return DOOR_GROUP_ITEM_IDS[name] +def get_panel_door_item_id(room: str, name: str): + if room not in PANEL_DOOR_ITEM_IDS or name not in PANEL_DOOR_ITEM_IDS[room]: + raise Exception(f"Item ID for panel door {room} - {name} not found in ids.yaml.") + + return PANEL_DOOR_ITEM_IDS[room][name] + + +def get_panel_group_item_id(name: str): + if name not in PANEL_GROUP_ITEM_IDS: + raise Exception(f"Item ID for panel group {name} not found in ids.yaml.") + + return PANEL_GROUP_ITEM_IDS[name] + + def get_progressive_item_id(name: str): if name not in PROGRESSIVE_ITEM_IDS: raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.") @@ -89,7 +107,7 @@ def find_class(self, module, name): return getattr(safe_builtins, name) raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") - file = pkgutil.get_data(__name__, os.path.join("data", "generated.dat")) + file = pkgutil.get_data(__name__, "data/generated.dat") pickdata = RenameUnpickler(BytesIO(file)).load() HASHES.update(pickdata["HASHES"]) @@ -97,8 +115,10 @@ def find_class(self, module, name): ALL_ROOMS.extend(pickdata["ALL_ROOMS"]) DOORS_BY_ROOM.update(pickdata["DOORS_BY_ROOM"]) PANELS_BY_ROOM.update(pickdata["PANELS_BY_ROOM"]) - PROGRESSIVE_ITEMS.extend(pickdata["PROGRESSIVE_ITEMS"]) - PROGRESSION_BY_ROOM.update(pickdata["PROGRESSION_BY_ROOM"]) + PANEL_DOORS_BY_ROOM.update(pickdata["PANEL_DOORS_BY_ROOM"]) + PROGRESSIVE_ITEMS.update(pickdata["PROGRESSIVE_ITEMS"]) + PROGRESSIVE_DOORS_BY_ROOM.update(pickdata["PROGRESSIVE_DOORS_BY_ROOM"]) + PROGRESSIVE_PANELS_BY_ROOM.update(pickdata["PROGRESSIVE_PANELS_BY_ROOM"]) PAINTING_ENTRANCES = pickdata["PAINTING_ENTRANCES"] PAINTING_EXIT_ROOMS.update(pickdata["PAINTING_EXIT_ROOMS"]) PAINTING_EXITS = pickdata["PAINTING_EXITS"] @@ -111,6 +131,8 @@ def find_class(self, module, name): DOOR_LOCATION_IDS.update(pickdata["DOOR_LOCATION_IDS"]) DOOR_ITEM_IDS.update(pickdata["DOOR_ITEM_IDS"]) DOOR_GROUP_ITEM_IDS.update(pickdata["DOOR_GROUP_ITEM_IDS"]) + PANEL_DOOR_ITEM_IDS.update(pickdata["PANEL_DOOR_ITEM_IDS"]) + PANEL_GROUP_ITEM_IDS.update(pickdata["PANEL_GROUP_ITEM_IDS"]) PROGRESSIVE_ITEM_IDS.update(pickdata["PROGRESSIVE_ITEM_IDS"]) diff --git a/worlds/lingo/test/TestDatafile.py b/worlds/lingo/test/TestDatafile.py index 60acb3e85e96..a01ff4171786 100644 --- a/worlds/lingo/test/TestDatafile.py +++ b/worlds/lingo/test/TestDatafile.py @@ -1,7 +1,7 @@ import os import unittest -from ..static_logic import HASHES +from ..static_logic import HASHES, PANELS_BY_ROOM from ..utils.pickle_static_data import hash_file @@ -14,3 +14,8 @@ def test_check_hashes(self) -> None: "LL1.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'") self.assertEqual(ids_file_hash, HASHES["ids.yaml"], "ids.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'") + + def test_panel_doors_are_set(self) -> None: + # This panel is defined earlier in the file than the panel door, so we want to check that the panel door is + # correctly applied. + self.assertNotEqual(PANELS_BY_ROOM["Outside The Agreeable"]["FIVE (1)"].panel_door, None) diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py index f496c5f5785a..cfbd7f30278c 100644 --- a/worlds/lingo/test/TestDoors.py +++ b/worlds/lingo/test/TestDoors.py @@ -3,7 +3,7 @@ class TestRequiredRoomLogic(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "shuffle_colors": "false", } @@ -50,7 +50,7 @@ def test_hidden_first(self) -> None: class TestRequiredDoorLogic(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "shuffle_colors": "false", } @@ -78,7 +78,8 @@ def test_through_hidden(self) -> None: class TestSimpleDoors(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "shuffle_colors": "false", } @@ -90,3 +91,52 @@ def test_requirement(self): self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) + +class TestPanels(LingoTestBase): + options = { + "shuffle_doors": "panels" + } + + def test_requirement(self): + self.assertFalse(self.can_reach_location("Starting Room - HIDDEN")) + self.assertFalse(self.can_reach_location("Hidden Room - OPEN")) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Starting Room - HIDDEN (Panel)") + self.assertTrue(self.can_reach_location("Starting Room - HIDDEN")) + self.assertFalse(self.can_reach_location("Hidden Room - OPEN")) + self.assertFalse(self.can_reach_location("The Seeker - Achievement")) + + self.collect_by_name("Hidden Room - OPEN (Panel)") + self.assertTrue(self.can_reach_location("Starting Room - HIDDEN")) + self.assertTrue(self.can_reach_location("Hidden Room - OPEN")) + self.assertTrue(self.can_reach_location("The Seeker - Achievement")) + + +class TestGroupedPanels(LingoTestBase): + options = { + "shuffle_doors": "panels", + "group_doors": "true", + "shuffle_colors": "false", + } + + def test_requirement(self): + self.assertFalse(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertFalse(self.can_reach_location("Dread Hallway - DREAD")) + self.assertFalse(self.can_reach_location("The Tenacious - Achievement")) + + self.collect_by_name("Tenacious Entrance Panels") + self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertFalse(self.can_reach_location("Dread Hallway - DREAD")) + self.assertFalse(self.can_reach_location("The Tenacious - Achievement")) + + self.collect_by_name("Outside The Agreeable - BLACK (Panel)") + self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertTrue(self.can_reach_location("Dread Hallway - DREAD")) + self.assertFalse(self.can_reach_location("The Tenacious - Achievement")) + + self.collect_by_name("The Tenacious - Black Palindromes (Panels)") + self.assertTrue(self.can_reach_location("Hub Room - SLAUGHTER")) + self.assertTrue(self.can_reach_location("Dread Hallway - DREAD")) + self.assertTrue(self.can_reach_location("The Tenacious - Achievement")) + diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py index 3ebe40aa22d7..c9c79a9d0658 100644 --- a/worlds/lingo/test/TestMastery.py +++ b/worlds/lingo/test/TestMastery.py @@ -5,7 +5,8 @@ class TestMasteryWhenVictoryIsTheEnd(LingoTestBase): options = { "mastery_achievements": "22", "victory_condition": "the_end", - "shuffle_colors": "true" + "shuffle_colors": "true", + "shuffle_postgame": "true", } def test_requirement(self): @@ -43,7 +44,8 @@ class TestMasteryBlocksDependents(LingoTestBase): options = { "mastery_achievements": "24", "shuffle_colors": "true", - "location_checks": "insanity" + "location_checks": "insanity", + "victory_condition": "level_2", } def test_requirement(self): diff --git a/worlds/lingo/test/TestOptions.py b/worlds/lingo/test/TestOptions.py index fce074311637..bd8ed81d7a12 100644 --- a/worlds/lingo/test/TestOptions.py +++ b/worlds/lingo/test/TestOptions.py @@ -3,7 +3,7 @@ class TestMultiShuffleOptions(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "progressive_orange_tower": "true", "shuffle_colors": "true", "shuffle_paintings": "true", @@ -13,7 +13,7 @@ class TestMultiShuffleOptions(LingoTestBase): class TestPanelsanity(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "progressive_orange_tower": "true", "location_checks": "insanity", "shuffle_colors": "true" @@ -22,7 +22,18 @@ class TestPanelsanity(LingoTestBase): class TestAllPanelHunt(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "progressive_orange_tower": "true", + "shuffle_colors": "true", + "victory_condition": "level_2", + "level_2_requirement": "800", + "early_color_hallways": "true" + } + + +class TestAllPanelHuntPanelsMode(LingoTestBase): + options = { + "shuffle_doors": "panels", "progressive_orange_tower": "true", "shuffle_colors": "true", "victory_condition": "level_2", diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py index 7b0c3bb52518..444264a58964 100644 --- a/worlds/lingo/test/TestOrangeTower.py +++ b/worlds/lingo/test/TestOrangeTower.py @@ -3,7 +3,7 @@ class TestProgressiveOrangeTower(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "progressive_orange_tower": "true" } diff --git a/worlds/lingo/test/TestPanelsanity.py b/worlds/lingo/test/TestPanelsanity.py index 34c1b3815a46..f8330ae78206 100644 --- a/worlds/lingo/test/TestPanelsanity.py +++ b/worlds/lingo/test/TestPanelsanity.py @@ -3,7 +3,7 @@ class TestPanelHunt(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "location_checks": "insanity", "victory_condition": "level_2", "level_2_requirement": "15" diff --git a/worlds/lingo/test/TestPilgrimage.py b/worlds/lingo/test/TestPilgrimage.py index 3cc91940017e..328156da2d17 100644 --- a/worlds/lingo/test/TestPilgrimage.py +++ b/worlds/lingo/test/TestPilgrimage.py @@ -18,7 +18,7 @@ class TestPilgrimageWithRoofAndPaintings(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", - "shuffle_doors": "complex", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "true", "pilgrimage_allows_paintings": "true", "early_color_hallways": "false" @@ -29,7 +29,6 @@ def test_access(self): "Outside The Undeterred - Green Painting"] for door in doors: - print(door) self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM")) self.collect_by_name(door) @@ -40,7 +39,7 @@ class TestPilgrimageNoRoofYesPaintings(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", - "shuffle_doors": "complex", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "false", "pilgrimage_allows_paintings": "true", "early_color_hallways": "false" @@ -53,7 +52,6 @@ def test_access(self): "Starting Room - Street Painting"] for door in doors: - print(door) self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM")) self.collect_by_name(door) @@ -64,7 +62,7 @@ class TestPilgrimageNoRoofNoPaintings(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", - "shuffle_doors": "complex", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "false", "pilgrimage_allows_paintings": "false", "early_color_hallways": "false" @@ -81,18 +79,45 @@ def test_access(self): "Orange Tower Fourth Floor - Hot Crusts Door"] for door in doors: - print(door) self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM")) self.collect_by_name(door) self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM")) -class TestPilgrimageYesRoofNoPaintings(LingoTestBase): +class TestPilgrimageRequireStartingRoom(LingoTestBase): options = { "enable_pilgrimage": "true", "shuffle_colors": "false", "shuffle_doors": "complex", + "pilgrimage_allows_roof_access": "false", + "pilgrimage_allows_paintings": "false", + "early_color_hallways": "false" + } + + def test_access(self): + doors = ["Second Room - Exit Door", "Crossroads - Roof Access", "Hub Room - Crossroads Entrance", + "Outside The Undeterred - Green Painting", "Outside The Undeterred - Number Hunt", + "Starting Room - Street Painting", "Outside The Initiated - Shortcut to Hub Room", + "Directional Gallery - Shortcut to The Undeterred", "Orange Tower First Floor - Salt Pepper Door", + "Color Hunt - Shortcut to The Steady", "The Bearer - Entrance", + "Orange Tower Fifth Floor - Quadruple Intersection", "The Tenacious - Shortcut to Hub Room", + "Outside The Agreeable - Tenacious Entrance", "Crossroads - Tower Entrance", + "Orange Tower Fourth Floor - Hot Crusts Door", "Challenge Room - Welcome Door", + "Number Hunt - Challenge Entrance", "Welcome Back Area - Shortcut to Starting Room"] + + for door in doors: + self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM")) + self.collect_by_name(door) + + self.assertTrue(self.can_reach_location("Pilgrim Antechamber - PILGRIM")) + + +class TestPilgrimageYesRoofNoPaintings(LingoTestBase): + options = { + "enable_pilgrimage": "true", + "shuffle_colors": "false", + "shuffle_doors": "doors", "pilgrimage_allows_roof_access": "true", "pilgrimage_allows_paintings": "false", "early_color_hallways": "false" @@ -107,7 +132,6 @@ def test_access(self): "Orange Tower Fifth Floor - Quadruple Intersection"] for door in doors: - print(door) self.assertFalse(self.can_reach_location("Pilgrim Antechamber - PILGRIM")) self.collect_by_name(door) diff --git a/worlds/lingo/test/TestPostgame.py b/worlds/lingo/test/TestPostgame.py new file mode 100644 index 000000000000..d2e2232ff769 --- /dev/null +++ b/worlds/lingo/test/TestPostgame.py @@ -0,0 +1,62 @@ +from . import LingoTestBase + + +class TestPostgameVanillaTheEnd(LingoTestBase): + options = { + "shuffle_doors": "none", + "victory_condition": "the_end", + "shuffle_postgame": "false", + } + + def test_requirement(self): + location_names = [location.name for location in self.multiworld.get_locations(self.player)] + + self.assertTrue("The End (Solved)" in location_names) + self.assertTrue("Champion's Rest - YOU" in location_names) + self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names) + self.assertFalse("The Red - Achievement" in location_names) + + +class TestPostgameComplexDoorsTheEnd(LingoTestBase): + options = { + "shuffle_doors": "complex", + "victory_condition": "the_end", + "shuffle_postgame": "false", + } + + def test_requirement(self): + location_names = [location.name for location in self.multiworld.get_locations(self.player)] + + self.assertTrue("The End (Solved)" in location_names) + self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names) + self.assertTrue("The Red - Achievement" in location_names) + + +class TestPostgameLateColorHunt(LingoTestBase): + options = { + "shuffle_doors": "none", + "victory_condition": "the_end", + "sunwarp_access": "disabled", + "shuffle_postgame": "false", + } + + def test_requirement(self): + location_names = [location.name for location in self.multiworld.get_locations(self.player)] + + self.assertFalse("Champion's Rest - YOU" in location_names) + + +class TestPostgameVanillaTheMaster(LingoTestBase): + options = { + "shuffle_doors": "none", + "victory_condition": "the_master", + "shuffle_postgame": "false", + } + + def test_requirement(self): + location_names = [location.name for location in self.multiworld.get_locations(self.player)] + + self.assertTrue("Orange Tower Seventh Floor - THE END" in location_names) + self.assertTrue("Orange Tower Seventh Floor - Mastery Achievements" in location_names) + self.assertTrue("The Red - Achievement" in location_names) + self.assertFalse("Mastery Panels" in location_names) diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py index e79fd6bc9087..2c837f53f34d 100644 --- a/worlds/lingo/test/TestProgressive.py +++ b/worlds/lingo/test/TestProgressive.py @@ -3,7 +3,7 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase): options = { - "shuffle_doors": "complex" + "shuffle_doors": "doors" } def test_item(self): @@ -54,7 +54,8 @@ def test_item(self): class TestSimpleHallwayRoom(LingoTestBase): options = { - "shuffle_doors": "simple" + "shuffle_doors": "doors", + "group_doors": "true", } def test_item(self): @@ -81,7 +82,7 @@ def test_item(self): class TestProgressiveArtGallery(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", "shuffle_colors": "false", } diff --git a/worlds/lingo/test/TestSunwarps.py b/worlds/lingo/test/TestSunwarps.py index e8e913c4f499..66ba3afd6e90 100644 --- a/worlds/lingo/test/TestSunwarps.py +++ b/worlds/lingo/test/TestSunwarps.py @@ -19,7 +19,8 @@ def test_access(self): class TestSimpleDoorsNormalSunwarps(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "sunwarp_access": "normal" } @@ -37,7 +38,8 @@ def test_access(self): class TestSimpleDoorsDisabledSunwarps(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "sunwarp_access": "disabled" } @@ -56,7 +58,8 @@ def test_access(self): class TestSimpleDoorsUnlockSunwarps(LingoTestBase): options = { - "shuffle_doors": "simple", + "shuffle_doors": "doors", + "group_doors": "true", "sunwarp_access": "unlock" } @@ -78,7 +81,8 @@ def test_access(self): class TestComplexDoorsNormalSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "normal" } @@ -96,7 +100,8 @@ def test_access(self): class TestComplexDoorsDisabledSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "disabled" } @@ -115,7 +120,8 @@ def test_access(self): class TestComplexDoorsIndividualSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "individual" } @@ -142,7 +148,8 @@ def test_access(self): class TestComplexDoorsProgressiveSunwarps(LingoTestBase): options = { - "shuffle_doors": "complex", + "shuffle_doors": "doors", + "group_doors": "false", "sunwarp_access": "progressive" } diff --git a/worlds/lingo/utils/assign_ids.rb b/worlds/lingo/utils/assign_ids.rb index 9e1ce67bd2db..f7de3d03f582 100644 --- a/worlds/lingo/utils/assign_ids.rb +++ b/worlds/lingo/utils/assign_ids.rb @@ -73,6 +73,22 @@ end end end +if old_generated.include? "panel_doors" then + old_generated["panel_doors"].each do |room, panel_doors| + panel_doors.each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end + end +end +if old_generated.include? "panel_groups" then + old_generated["panel_groups"].each do |name, id| + if id >= next_item_id then + next_item_id = id + 1 + end + end +end if old_generated.include? "progression" then old_generated["progression"].each do |name, id| if id >= next_item_id then @@ -82,6 +98,7 @@ end door_groups = Set[] +panel_groups = Set[] config = YAML.load_file(configpath) config.each do |room_name, room_data| @@ -163,6 +180,29 @@ end end + if room_data.include? "panel_doors" + room_data["panel_doors"].each do |panel_door_name, panel_door| + unless old_generated.include? "panel_doors" and old_generated["panel_doors"].include? room_name and old_generated["panel_doors"][room_name].include? panel_door_name then + old_generated["panel_doors"] ||= {} + old_generated["panel_doors"][room_name] ||= {} + old_generated["panel_doors"][room_name][panel_door_name] = next_item_id + + next_item_id += 1 + end + + if panel_door.include? "panel_group" and not panel_groups.include? panel_door["panel_group"] then + panel_groups.add(panel_door["panel_group"]) + + unless old_generated.include? "panel_groups" and old_generated["panel_groups"].include? panel_door["panel_group"] then + old_generated["panel_groups"] ||= {} + old_generated["panel_groups"][panel_door["panel_group"]] = next_item_id + + next_item_id += 1 + end + end + end + end + if room_data.include? "progression" room_data["progression"].each do |progression_name, pdata| unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py index e40c21ce3e6a..df82a12861a4 100644 --- a/worlds/lingo/utils/pickle_static_data.py +++ b/worlds/lingo/utils/pickle_static_data.py @@ -6,22 +6,23 @@ sys.path.append(os.path.join("worlds", "lingo")) sys.path.append(".") sys.path.append("..") -from datatypes import Door, DoorType, EntranceType, Painting, Panel, Progression, Room, RoomAndDoor, RoomAndPanel,\ - RoomEntrance +from datatypes import Door, DoorType, EntranceType, Painting, Panel, PanelDoor, Progression, Room, RoomAndDoor,\ + RoomAndPanel, RoomAndPanelDoor, RoomEntrance import hashlib import pickle -import sys import Utils ALL_ROOMS: List[Room] = [] DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {} PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {} +PANEL_DOORS_BY_ROOM: Dict[str, Dict[str, PanelDoor]] = {} PAINTINGS: Dict[str, Painting] = {} -PROGRESSIVE_ITEMS: List[str] = [] -PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_ITEMS: Set[str] = set() +PROGRESSIVE_DOORS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} +PROGRESSIVE_PANELS_BY_ROOM: Dict[str, Dict[str, Progression]] = {} PAINTING_ENTRANCES: int = 0 PAINTING_EXIT_ROOMS: Set[str] = set() @@ -37,8 +38,13 @@ DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {} DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} DOOR_GROUP_ITEM_IDS: Dict[str, int] = {} +PANEL_DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {} +PANEL_GROUP_ITEM_IDS: Dict[str, int] = {} PROGRESSIVE_ITEM_IDS: Dict[str, int] = {} +# This doesn't need to be stored in the datafile. +PANEL_DOOR_BY_PANEL_BY_ROOM: Dict[str, Dict[str, str]] = {} + def hash_file(path): md5 = hashlib.md5() @@ -53,7 +59,7 @@ def hash_file(path): def load_static_data(ll1_path, ids_path): global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \ - DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS + DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS, PANEL_DOOR_ITEM_IDS, PANEL_GROUP_ITEM_IDS # Load in all item and location IDs. These are broken up into groups based on the type of item/location. with open(ids_path, "r") as file: @@ -86,6 +92,17 @@ def load_static_data(ll1_path, ids_path): for item_name, item_id in config["door_groups"].items(): DOOR_GROUP_ITEM_IDS[item_name] = item_id + if "panel_doors" in config: + for room_name, panel_doors in config["panel_doors"].items(): + PANEL_DOOR_ITEM_IDS[room_name] = {} + + for panel_door, item_id in panel_doors.items(): + PANEL_DOOR_ITEM_IDS[room_name][panel_door] = item_id + + if "panel_groups" in config: + for item_name, item_id in config["panel_groups"].items(): + PANEL_GROUP_ITEM_IDS[item_name] = item_id + if "progression" in config: for item_name, item_id in config["progression"].items(): PROGRESSIVE_ITEM_IDS[item_name] = item_id @@ -94,6 +111,16 @@ def load_static_data(ll1_path, ids_path): with open(ll1_path, "r") as file: config = Utils.parse_yaml(file) + # We have to process all panel doors first so that panels can see what panel doors they're in even if they're + # defined earlier in the file than the panel door. + for room_name, room_data in config.items(): + if "panel_doors" in room_data: + PANEL_DOORS_BY_ROOM[room_name] = dict() + + for panel_door_name, panel_door_data in room_data["panel_doors"].items(): + process_panel_door(room_name, panel_door_name, panel_door_data) + + # Process the rest of the room. for room_name, room_data in config.items(): process_room(room_name, room_data) @@ -147,6 +174,46 @@ def process_entrance(source_room, doors, room_obj): room_obj.entrances.append(RoomEntrance(source_room, door, entrance_type)) +def process_panel_door(room_name, panel_door_name, panel_door_data): + global PANEL_DOORS_BY_ROOM, PANEL_DOOR_BY_PANEL_BY_ROOM + + panels: List[RoomAndPanel] = list() + for panel in panel_door_data["panels"]: + if isinstance(panel, dict): + panels.append(RoomAndPanel(panel["room"], panel["panel"])) + else: + panels.append(RoomAndPanel(room_name, panel)) + + for panel in panels: + PANEL_DOOR_BY_PANEL_BY_ROOM.setdefault(panel.room, {})[panel.panel] = RoomAndPanelDoor(room_name, + panel_door_name) + + if "item_name" in panel_door_data: + item_name = panel_door_data["item_name"] + else: + panel_per_room = dict() + for panel in panels: + panel_room_name = room_name if panel.room is None else panel.room + panel_per_room.setdefault(panel_room_name, []).append(panel.panel) + + room_strs = list() + for door_room_str, door_panels_str in panel_per_room.items(): + room_strs.append(door_room_str + " - " + ", ".join(door_panels_str)) + + if len(panels) == 1: + item_name = f"{room_strs[0]} (Panel)" + else: + item_name = " and ".join(room_strs) + " (Panels)" + + if "panel_group" in panel_door_data: + panel_group = panel_door_data["panel_group"] + else: + panel_group = None + + panel_door_obj = PanelDoor(item_name, panel_group) + PANEL_DOORS_BY_ROOM[room_name][panel_door_name] = panel_door_obj + + def process_panel(room_name, panel_name, panel_data): global PANELS_BY_ROOM @@ -227,13 +294,18 @@ def process_panel(room_name, panel_name, panel_data): else: non_counting = False + if room_name in PANEL_DOOR_BY_PANEL_BY_ROOM and panel_name in PANEL_DOOR_BY_PANEL_BY_ROOM[room_name]: + panel_door = PANEL_DOOR_BY_PANEL_BY_ROOM[room_name][panel_name] + else: + panel_door = None + if "location_name" in panel_data: location_name = panel_data["location_name"] else: location_name = None panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce, - achievement, non_counting, location_name) + achievement, non_counting, panel_door, location_name) PANELS_BY_ROOM[room_name][panel_name] = panel_obj @@ -325,7 +397,7 @@ def process_door(room_name, door_name, door_data): painting_ids = [] door_type = DoorType.NORMAL - if door_name.endswith(" Sunwarp"): + if room_name == "Sunwarps": door_type = DoorType.SUNWARP elif room_name == "Pilgrim Antechamber" and door_name == "Sun Painting": door_type = DoorType.SUN_PAINTING @@ -404,11 +476,11 @@ def process_sunwarp(room_name, sunwarp_data): SUNWARP_EXITS[sunwarp_data["dots"] - 1] = room_name -def process_progression(room_name, progression_name, progression_doors): - global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM +def process_progressive_door(room_name, progression_name, progression_doors): + global PROGRESSIVE_ITEMS, PROGRESSIVE_DOORS_BY_ROOM # Progressive items are configured as a list of doors. - PROGRESSIVE_ITEMS.append(progression_name) + PROGRESSIVE_ITEMS.add(progression_name) progression_index = 1 for door in progression_doors: @@ -419,11 +491,31 @@ def process_progression(room_name, progression_name, progression_doors): door_room = room_name door_door = door - room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {}) + room_progressions = PROGRESSIVE_DOORS_BY_ROOM.setdefault(door_room, {}) room_progressions[door_door] = Progression(progression_name, progression_index) progression_index += 1 +def process_progressive_panel(room_name, progression_name, progression_panel_doors): + global PROGRESSIVE_ITEMS, PROGRESSIVE_PANELS_BY_ROOM + + # Progressive items are configured as a list of panel doors. + PROGRESSIVE_ITEMS.add(progression_name) + + progression_index = 1 + for panel_door in progression_panel_doors: + if isinstance(panel_door, Dict): + panel_door_room = panel_door["room"] + panel_door_door = panel_door["panel_door"] + else: + panel_door_room = room_name + panel_door_door = panel_door + + room_progressions = PROGRESSIVE_PANELS_BY_ROOM.setdefault(panel_door_room, {}) + room_progressions[panel_door_door] = Progression(progression_name, progression_index) + progression_index += 1 + + def process_room(room_name, room_data): global ALL_ROOMS @@ -454,8 +546,11 @@ def process_room(room_name, room_data): process_sunwarp(room_name, sunwarp_data) if "progression" in room_data: - for progression_name, progression_doors in room_data["progression"].items(): - process_progression(room_name, progression_name, progression_doors) + for progression_name, pdata in room_data["progression"].items(): + if "doors" in pdata: + process_progressive_door(room_name, progression_name, pdata["doors"]) + if "panel_doors" in pdata: + process_progressive_panel(room_name, progression_name, pdata["panel_doors"]) ALL_ROOMS.append(room_obj) @@ -492,8 +587,10 @@ def process_room(room_name, room_data): "ALL_ROOMS": ALL_ROOMS, "DOORS_BY_ROOM": DOORS_BY_ROOM, "PANELS_BY_ROOM": PANELS_BY_ROOM, + "PANEL_DOORS_BY_ROOM": PANEL_DOORS_BY_ROOM, "PROGRESSIVE_ITEMS": PROGRESSIVE_ITEMS, - "PROGRESSION_BY_ROOM": PROGRESSION_BY_ROOM, + "PROGRESSIVE_DOORS_BY_ROOM": PROGRESSIVE_DOORS_BY_ROOM, + "PROGRESSIVE_PANELS_BY_ROOM": PROGRESSIVE_PANELS_BY_ROOM, "PAINTING_ENTRANCES": PAINTING_ENTRANCES, "PAINTING_EXIT_ROOMS": PAINTING_EXIT_ROOMS, "PAINTING_EXITS": PAINTING_EXITS, @@ -506,6 +603,8 @@ def process_room(room_name, room_data): "DOOR_LOCATION_IDS": DOOR_LOCATION_IDS, "DOOR_ITEM_IDS": DOOR_ITEM_IDS, "DOOR_GROUP_ITEM_IDS": DOOR_GROUP_ITEM_IDS, + "PANEL_DOOR_ITEM_IDS": PANEL_DOOR_ITEM_IDS, + "PANEL_GROUP_ITEM_IDS": PANEL_GROUP_ITEM_IDS, "PROGRESSIVE_ITEM_IDS": PROGRESSIVE_ITEM_IDS, } diff --git a/worlds/lingo/utils/validate_config.rb b/worlds/lingo/utils/validate_config.rb index 498980bb719a..70f7fc2cf659 100644 --- a/worlds/lingo/utils/validate_config.rb +++ b/worlds/lingo/utils/validate_config.rb @@ -33,19 +33,23 @@ configured_rooms = Set["Menu"] configured_doors = Set[] configured_panels = Set[] +configured_panel_doors = Set[] mentioned_rooms = Set[] mentioned_doors = Set[] mentioned_panels = Set[] +mentioned_panel_doors = Set[] mentioned_sunwarp_entrances = Set[] mentioned_sunwarp_exits = Set[] mentioned_paintings = Set[] door_groups = {} +panel_groups = {} -directives = Set["entrances", "panels", "doors", "paintings", "sunwarps", "progression"] +directives = Set["entrances", "panels", "doors", "panel_doors", "paintings", "sunwarps", "progression"] panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt", "location_name"] door_directives = Set["id", "painting_id", "panels", "item_name", "item_group", "location_name", "skip_location", "skip_item", "door_group", "include_reduce", "event", "warp_id"] +panel_door_directives = Set["panels", "item_name", "panel_group"] painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"] non_counting = 0 @@ -253,6 +257,43 @@ end end + (room["panel_doors"] || {}).each do |panel_door_name, panel_door| + configured_panel_doors.add("#{room_name} - #{panel_door_name}") + + if panel_door.include?("panels") + panel_door["panels"].each do |panel| + if panel.kind_of? Hash then + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{other_room} - #{panel["panel"]}") + else + other_room = panel.include?("room") ? panel["room"] : room_name + mentioned_panels.add("#{room_name} - #{panel}") + end + end + else + puts "#{room_name} - #{panel_door_name} :::: Missing panels field" + end + + if panel_door.include?("panel_group") + panel_groups[panel_door["panel_group"]] ||= 0 + panel_groups[panel_door["panel_group"]] += 1 + end + + bad_subdirectives = [] + panel_door.keys.each do |key| + unless panel_door_directives.include?(key) then + bad_subdirectives << key + end + end + unless bad_subdirectives.empty? then + puts "#{room_name} - #{panel_door_name} :::: Panel door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}" + end + + unless ids.include?("panel_doors") and ids["panel_doors"].include?(room_name) and ids["panel_doors"][room_name].include?(panel_door_name) + puts "#{room_name} - #{panel_door_name} :::: Panel door is missing an item ID" + end + end + (room["paintings"] || []).each do |painting| if painting.include?("id") and painting["id"].kind_of? String then unless paintings.include? painting["id"] then @@ -327,12 +368,24 @@ end end - (room["progression"] || {}).each do |progression_name, door_list| - door_list.each do |door| - if door.kind_of? Hash then - mentioned_doors.add("#{door["room"]} - #{door["door"]}") - else - mentioned_doors.add("#{room_name} - #{door}") + (room["progression"] || {}).each do |progression_name, pdata| + if pdata.include? "doors" then + pdata["doors"].each do |door| + if door.kind_of? Hash then + mentioned_doors.add("#{door["room"]} - #{door["door"]}") + else + mentioned_doors.add("#{room_name} - #{door}") + end + end + end + + if pdata.include? "panel_doors" then + pdata["panel_doors"].each do |panel_door| + if panel_door.kind_of? Hash then + mentioned_panel_doors.add("#{panel_door["room"]} - #{panel_door["panel_door"]}") + else + mentioned_panel_doors.add("#{room_name} - #{panel_door}") + end end end @@ -344,17 +397,22 @@ errored_rooms = mentioned_rooms - configured_rooms unless errored_rooms.empty? then - puts "The folloring rooms are mentioned but do not exist: " + errored_rooms.to_s + puts "The following rooms are mentioned but do not exist: " + errored_rooms.to_s end errored_panels = mentioned_panels - configured_panels unless errored_panels.empty? then - puts "The folloring panels are mentioned but do not exist: " + errored_panels.to_s + puts "The following panels are mentioned but do not exist: " + errored_panels.to_s end errored_doors = mentioned_doors - configured_doors unless errored_doors.empty? then - puts "The folloring doors are mentioned but do not exist: " + errored_doors.to_s + puts "The following doors are mentioned but do not exist: " + errored_doors.to_s +end + +errored_panel_doors = mentioned_panel_doors - configured_panel_doors +unless errored_panel_doors.empty? then + puts "The following panel doors are mentioned but do not exist: " + errored_panel_doors.to_s end door_groups.each do |group,num| @@ -367,6 +425,16 @@ end end +panel_groups.each do |group,num| + if num == 1 then + puts "Panel group \"#{group}\" only has one panel in it" + end + + unless ids.include?("panel_groups") and ids["panel_groups"].include?(group) + puts "#{group} :::: Panel group is missing an item ID" + end +end + slashed_rooms = configured_rooms.select do |room| room.include? "/" end diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 6433452cefea..96de24a4b6a0 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -118,7 +118,7 @@ def create_regions(self) -> None: L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player)) chest_access.show_in_spoiler = False ancient_dungeon.locations.append(chest_access) - for iris in self.item_name_groups["Iris treasures"]: + for iris in sorted(self.item_name_groups["Iris treasures"]): treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}" iris_treasure: Location = \ L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon) diff --git a/worlds/lufia2ac/test/__init__.py b/worlds/lufia2ac/test/__init__.py index 24925675e36b..306ffa771660 100644 --- a/worlds/lufia2ac/test/__init__.py +++ b/worlds/lufia2ac/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class L2ACTestBase(WorldTestBase): diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index a03c33c2f7b6..59e724d3fb7f 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,5 +1,5 @@ import logging -from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO +from typing import Any, ClassVar, TextIO from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial from Options import Accessibility @@ -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") @@ -119,16 +120,16 @@ class MessengerWorld(World): required_seals: int = 0 created_seals: int = 0 total_shards: int = 0 - shop_prices: Dict[str, int] - figurine_prices: Dict[str, int] - _filler_items: List[str] - starting_portals: List[str] - plando_portals: List[str] - spoiler_portal_mapping: Dict[str, str] - portal_mapping: List[int] - transitions: List[Entrance] + shop_prices: dict[str, int] + figurine_prices: dict[str, int] + _filler_items: list[str] + starting_portals: list[str] + plando_portals: list[str] + spoiler_portal_mapping: dict[str, str] + portal_mapping: list[int] + transitions: list[Entrance] reachable_locs: int = 0 - filler: Dict[str, int] + filler: dict[str, int] def generate_early(self) -> None: if self.options.goal == Goal.option_power_seal_hunt: @@ -177,7 +178,7 @@ def create_regions(self) -> None: for reg_name in sub_region] for region in complex_regions: - region_name = region.name.replace(f"{region.parent} - ", "") + region_name = region.name.removeprefix(f"{region.parent} - ") connection_data = CONNECTIONS[region.parent][region_name] for exit_region in connection_data: region.connect(self.multiworld.get_region(exit_region, self.player)) @@ -190,7 +191,7 @@ def create_items(self) -> None: # create items that are always in the item pool main_movement_items = ["Rope Dart", "Wingsuit"] precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]] - itempool: List[MessengerItem] = [ + itempool: list[MessengerItem] = [ self.create_item(item) for item in self.item_name_to_id if item not in { @@ -289,7 +290,7 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: for portal, output in portal_info: spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player) - def fill_slot_data(self) -> Dict[str, Any]: + def fill_slot_data(self) -> dict[str, Any]: slot_data = { "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}, "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}, @@ -315,7 +316,7 @@ def get_filler_item_name(self) -> str: return self._filler_items.pop(0) def create_item(self, name: str) -> MessengerItem: - item_id: Optional[int] = self.item_name_to_id.get(name, None) + item_id: int | None = self.item_name_to_id.get(name, None) return MessengerItem( name, ItemClassification.progression if item_id is None else self.get_item_classification(name), @@ -350,7 +351,7 @@ def get_item_classification(self, name: str) -> ItemClassification: return ItemClassification.filler @classmethod - def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World: + def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World: group = super().create_group(multiworld, new_player_id, players) assert isinstance(group, MessengerWorld) diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 9fd08e52d899..6b98a1b44013 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -1,11 +1,11 @@ +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 typing import Any from zipfile import ZipFile from Utils import open_file @@ -17,11 +17,32 @@ 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) -> bool | None: + """ + 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 +77,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 +152,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 +188,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..79912a5688c2 100644 --- a/worlds/messenger/connections.py +++ b/worlds/messenger/connections.py @@ -1,6 +1,4 @@ -from typing import Dict, List - -CONNECTIONS: Dict[str, Dict[str, List[str]]] = { +CONNECTIONS: dict[str, dict[str, list[str]]] = { "Ninja Village": { "Right": [ "Autumn Hills - Left", @@ -114,7 +112,6 @@ "Forlorn Temple - Rocket Maze Checkpoint", ], "Rocket Maze Checkpoint": [ - "Forlorn Temple - Sunny Day Checkpoint", "Forlorn Temple - Climb Shop", ], }, @@ -641,7 +638,7 @@ }, } -RANDOMIZED_CONNECTIONS: Dict[str, str] = { +RANDOMIZED_CONNECTIONS: dict[str, str] = { "Ninja Village - Right": "Autumn Hills - Left", "Autumn Hills - Left": "Ninja Village - Right", "Autumn Hills - Right": "Forlorn Temple - Left", @@ -681,7 +678,7 @@ "Sunken Shrine - Left": "Howling Grotto - Bottom", } -TRANSITIONS: List[str] = [ +TRANSITIONS: list[str] = [ "Ninja Village - Right", "Autumn Hills - Left", "Autumn Hills - Right", diff --git a/worlds/messenger/constants.py b/worlds/messenger/constants.py index ea15c71068db..47b5a1a85cff 100644 --- a/worlds/messenger/constants.py +++ b/worlds/messenger/constants.py @@ -2,7 +2,7 @@ # items # listing individual groups first for easy lookup -NOTES = [ +NOTES: list[str] = [ "Key of Hope", "Key of Chaos", "Key of Courage", @@ -11,7 +11,7 @@ "Key of Symbiosis", ] -PROG_ITEMS = [ +PROG_ITEMS: list[str] = [ "Wingsuit", "Rope Dart", "Lightfoot Tabi", @@ -28,18 +28,18 @@ "Seashell", ] -PHOBEKINS = [ +PHOBEKINS: list[str] = [ "Necro", "Pyro", "Claustro", "Acro", ] -USEFUL_ITEMS = [ +USEFUL_ITEMS: list[str] = [ "Windmill Shuriken", ] -FILLER = { +FILLER: dict[str, int] = { "Time Shard": 5, "Time Shard (10)": 10, "Time Shard (50)": 20, @@ -48,13 +48,13 @@ "Time Shard (500)": 5, } -TRAPS = { +TRAPS: dict[str, int] = { "Teleport Trap": 5, "Prophecy Trap": 10, } # item_name_to_id needs to be deterministic and match upstream -ALL_ITEMS = [ +ALL_ITEMS: list[str] = [ *NOTES, "Windmill Shuriken", "Wingsuit", @@ -83,7 +83,7 @@ # locations # the names of these don't actually matter, but using the upstream's names for now # order must be exactly the same as upstream -ALWAYS_LOCATIONS = [ +ALWAYS_LOCATIONS: list[str] = [ # notes "Sunken Shrine - Key of Love", "Corrupted Future - Key of Courage", @@ -160,7 +160,7 @@ "Elemental Skylands Seal - Fire", ] -BOSS_LOCATIONS = [ +BOSS_LOCATIONS: list[str] = [ "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem", 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/options.py b/worlds/messenger/options.py index 1f76dba4894a..8b61a9435422 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -1,17 +1,16 @@ from dataclasses import dataclass -from typing import Dict from schema import And, Optional, Or, Schema -from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ +from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \ PlandoConnections, Range, StartInventoryPool, Toggle, Visibility from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS -class MessengerAccessibility(Accessibility): - default = Accessibility.option_locations +class MessengerAccessibility(ItemsAccessibility): # defaulting to locations accessibility since items makes certain items self-locking - __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") + default = ItemsAccessibility.option_full + __doc__ = ItemsAccessibility.__doc__ class PortalPlando(PlandoConnections): @@ -167,7 +166,7 @@ class ShopPrices(Range): default = 100 -def planned_price(location: str) -> Dict[Optional, Or]: +def planned_price(location: str) -> dict[Optional, Or]: return { Optional(location): Or( And(int, lambda n: n >= 0), diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 1da210cb23ff..896fefa686f1 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import List, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions from Options import PlandoConnection @@ -8,7 +8,7 @@ from . import MessengerWorld -PORTALS = [ +PORTALS: list[str] = [ "Autumn Hills", "Riviere Turquoise", "Howling Grotto", @@ -18,7 +18,7 @@ ] -SHOP_POINTS = { +SHOP_POINTS: dict[str, list[str]] = { "Autumn Hills": [ "Climbing Claws", "Hope Path", @@ -113,7 +113,7 @@ } -CHECKPOINTS = { +CHECKPOINTS: dict[str, list[str]] = { "Autumn Hills": [ "Hope Latch", "Key of Hope", @@ -186,7 +186,7 @@ } -REGION_ORDER = [ +REGION_ORDER: list[str] = [ "Autumn Hills", "Forlorn Temple", "Catacombs", @@ -215,27 +215,30 @@ 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) return parent - def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: + 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/messenger/regions.py b/worlds/messenger/regions.py index 153f8510f1bd..d53b84fe3401 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -1,7 +1,4 @@ -from typing import Dict, List - - -LOCATIONS: Dict[str, List[str]] = { +LOCATIONS: dict[str, list[str]] = { "Ninja Village - Nest": [ "Ninja Village - Candle", "Ninja Village - Astral Seed", @@ -201,7 +198,7 @@ } -SUB_REGIONS: Dict[str, List[str]] = { +SUB_REGIONS: dict[str, list[str]] = { "Ninja Village": [ "Right", ], @@ -385,7 +382,7 @@ # order is slightly funky here for back compat -MEGA_SHARDS: Dict[str, List[str]] = { +MEGA_SHARDS: dict[str, list[str]] = { "Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"], "Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"], "Catacombs - Top Left": ["Catacombs Mega Shard"], @@ -414,7 +411,7 @@ } -REGION_CONNECTIONS: Dict[str, Dict[str, str]] = { +REGION_CONNECTIONS: dict[str, dict[str, str]] = { "Menu": {"Tower HQ": "Start Game"}, "Tower HQ": { "Autumn Hills - Portal": "ToTHQ Autumn Hills Portal", @@ -436,7 +433,7 @@ # regions that don't have sub-regions -LEVELS: List[str] = [ +LEVELS: list[str] = [ "Menu", "Tower HQ", "The Shop", diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 85b73dec4147..f09025c7edce 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,4 +1,4 @@ -from typing import Dict, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items @@ -12,9 +12,9 @@ class MessengerRules: player: int world: "MessengerWorld" - connection_rules: Dict[str, CollectionRule] - region_rules: Dict[str, CollectionRule] - location_rules: Dict[str, CollectionRule] + connection_rules: dict[str, CollectionRule] + region_rules: dict[str, CollectionRule] + location_rules: dict[str, CollectionRule] maximum_price: int required_seals: int @@ -220,6 +220,8 @@ def __init__(self, world: "MessengerWorld") -> None: } self.location_rules = { + # hq + "Money Wrench": self.can_shop, # ninja village "Ninja Village Seal - Tree House": self.has_dart, diff --git a/worlds/messenger/shop.py b/worlds/messenger/shop.py index 3c8c7bf6f21e..6ab72f9765f3 100644 --- a/worlds/messenger/shop.py +++ b/worlds/messenger/shop.py @@ -1,11 +1,11 @@ -from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union +from typing import NamedTuple, TYPE_CHECKING if TYPE_CHECKING: from . import MessengerWorld else: MessengerWorld = object -PROG_SHOP_ITEMS: List[str] = [ +PROG_SHOP_ITEMS: list[str] = [ "Path of Resilience", "Meditation", "Strike of the Ninja", @@ -14,7 +14,7 @@ "Aerobatics Warrior", ] -USEFUL_SHOP_ITEMS: List[str] = [ +USEFUL_SHOP_ITEMS: list[str] = [ "Karuta Plates", "Serendipitous Bodies", "Kusari Jacket", @@ -29,10 +29,10 @@ class ShopData(NamedTuple): internal_name: str min_price: int max_price: int - prerequisite: Optional[Union[str, Set[str]]] = None + prerequisite: str | set[str] | None = None -SHOP_ITEMS: Dict[str, ShopData] = { +SHOP_ITEMS: dict[str, ShopData] = { "Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200), "Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"), "Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"), @@ -56,7 +56,7 @@ class ShopData(NamedTuple): "Focused Power Sense": ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"), } -FIGURINES: Dict[str, ShopData] = { +FIGURINES: dict[str, ShopData] = { "Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500), "Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500), "Ountarde Figurine": ShopData("OUNTARDE", 100, 500), @@ -73,12 +73,12 @@ class ShopData(NamedTuple): } -def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]: +def shuffle_shop_prices(world: MessengerWorld) -> tuple[dict[str, int], dict[str, int]]: shop_price_mod = world.options.shop_price.value shop_price_planned = world.options.shop_price_plan - shop_prices: Dict[str, int] = {} - figurine_prices: Dict[str, int] = {} + shop_prices: dict[str, int] = {} + figurine_prices: dict[str, int] = {} for item, price in shop_price_planned.value.items(): if not isinstance(price, int): price = world.random.choices(list(price.keys()), weights=list(price.values()))[0] diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index b60aeb179feb..29e3ea8953ec 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -1,5 +1,5 @@ from functools import cached_property -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region from .regions import LOCATIONS, MEGA_SHARDS @@ -10,14 +10,14 @@ class MessengerEntrance(Entrance): - world: Optional["MessengerWorld"] = None + world: "MessengerWorld | None" = None class MessengerRegion(Region): parent: str entrance_type = MessengerEntrance - def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None: + def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None: super().__init__(name, world.player, world.multiworld) self.parent = parent locations = [] @@ -48,7 +48,7 @@ def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = N class MessengerLocation(Location): game = "The Messenger" - def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None: + def __init__(self, player: int, name: str, loc_id: int | None, parent: MessengerRegion) -> None: super().__init__(player, name, loc_id, parent) if loc_id is None: if name == "Rescue Phantom": @@ -59,7 +59,7 @@ def __init__(self, player: int, name: str, loc_id: Optional[int], parent: Messen class MessengerShopLocation(MessengerLocation): @cached_property def cost(self) -> int: - name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped + name = self.name.removeprefix("The Shop - ") world = self.parent_region.multiworld.worlds[self.player] shop_data = SHOP_ITEMS[name] if shop_data.prerequisite: diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index 971ff1763b47..21a0c352bff4 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -1,5 +1,6 @@ from typing import Dict +from BaseClasses import CollectionState from . import MessengerTestBase from ..shop import SHOP_ITEMS, FIGURINES @@ -76,7 +77,7 @@ def test_costs(self) -> None: loc = f"The Shop - {loc}" self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost) - self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS) + self.assertTrue(loc.removeprefix("The Shop - ") in SHOP_ITEMS) self.assertEqual(len(prices), len(SHOP_ITEMS)) figures = self.world.figurine_prices @@ -89,3 +90,15 @@ def test_costs(self) -> None: self.assertTrue(loc in FIGURINES) self.assertEqual(len(figures), len(FIGURINES)) + + max_cost_state = CollectionState(self.multiworld) + self.assertFalse(self.world.get_location("Money Wrench").can_reach(max_cost_state)) + prog_shards = [] + for item in self.multiworld.itempool: + if "Time Shard " in item.name: + value = int(item.name.strip("Time Shard ()")) + if value >= 100: + prog_shards.append(item) + for shard in prog_shards: + max_cost_state.collect(shard, True) + self.assertTrue(self.world.get_location("Money Wrench").can_reach(max_cost_state)) diff --git a/worlds/minecraft/Constants.py b/worlds/minecraft/Constants.py index 0d1101e802fd..1f7b6fa6acef 100644 --- a/worlds/minecraft/Constants.py +++ b/worlds/minecraft/Constants.py @@ -3,7 +3,7 @@ import pkgutil def load_data_file(*args) -> dict: - fname = os.path.join("data", *args) + fname = "/".join(["data", *args]) return json.loads(pkgutil.get_data(__name__, fname).decode()) # For historical reasons, these values are different. diff --git a/worlds/minecraft/ItemPool.py b/worlds/minecraft/ItemPool.py index 78eeffca80f5..19bb70ed6402 100644 --- a/worlds/minecraft/ItemPool.py +++ b/worlds/minecraft/ItemPool.py @@ -1,10 +1,14 @@ from math import ceil from typing import List -from BaseClasses import MultiWorld, Item -from worlds.AutoWorld import World +from BaseClasses import Item from . import Constants +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MinecraftWorld + def get_junk_item_names(rand, k: int) -> str: junk_weights = Constants.item_info["junk_weights"] @@ -14,39 +18,38 @@ def get_junk_item_names(rand, k: int) -> str: k=k) return junk -def build_item_pool(mc_world: World) -> List[Item]: - multiworld = mc_world.multiworld - player = mc_world.player +def build_item_pool(world: "MinecraftWorld") -> List[Item]: + multiworld = world.multiworld + player = world.player itempool = [] total_location_count = len(multiworld.get_unfilled_locations(player)) required_pool = Constants.item_info["required_pool"] - junk_weights = Constants.item_info["junk_weights"] # Add required progression items for item_name, num in required_pool.items(): - itempool += [mc_world.create_item(item_name) for _ in range(num)] + itempool += [world.create_item(item_name) for _ in range(num)] # Add structure compasses - if multiworld.structure_compasses[player]: - compasses = [name for name in mc_world.item_name_to_id if "Structure Compass" in name] + if world.options.structure_compasses: + compasses = [name for name in world.item_name_to_id if "Structure Compass" in name] for item_name in compasses: - itempool.append(mc_world.create_item(item_name)) + itempool.append(world.create_item(item_name)) # Dragon egg shards - if multiworld.egg_shards_required[player] > 0: - num = multiworld.egg_shards_available[player] - itempool += [mc_world.create_item("Dragon Egg Shard") for _ in range(num)] + if world.options.egg_shards_required > 0: + num = world.options.egg_shards_available + itempool += [world.create_item("Dragon Egg Shard") for _ in range(num)] # Bee traps - bee_trap_percentage = multiworld.bee_traps[player] * 0.01 + bee_trap_percentage = world.options.bee_traps * 0.01 if bee_trap_percentage > 0: bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool))) - itempool += [mc_world.create_item("Bee Trap") for _ in range(bee_trap_qty)] + itempool += [world.create_item("Bee Trap") for _ in range(bee_trap_qty)] # Fill remaining itempool with randomly generated junk - junk = get_junk_item_names(multiworld.random, total_location_count - len(itempool)) - itempool += [mc_world.create_item(name) for name in junk] + junk = get_junk_item_names(world.random, total_location_count - len(itempool)) + itempool += [world.create_item(name) for name in junk] return itempool diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 9407097b4638..7d1377233e4c 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -1,6 +1,7 @@ -import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections +from Options import Choice, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections, \ + PerGameCommonOptions from .Constants import region_info +from dataclasses import dataclass class AdvancementGoal(Range): @@ -55,7 +56,7 @@ class StructureCompasses(DefaultOnToggle): display_name = "Structure Compasses" -class BeeTraps(Range): +class BeeTraps(Range): """Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when received.""" display_name = "Bee Trap Percentage" @@ -94,7 +95,20 @@ class SendDefeatedMobs(Toggle): class StartingItems(OptionList): - """Start with these items. Each entry should be of this format: {item: "item_name", amount: #, nbt: "nbt_string"}""" + """Start with these items. Each entry should be of this format: {item: "item_name", amount: #} + `item` can include components, and should be in an identical format to a `/give` command with + `"` escaped for json reasons. + + `amount` is optional and will default to 1 if omitted. + + example: + ``` + starting_items: [ + { "item": "minecraft:stick[minecraft:custom_name=\"{'text':'pointy stick'}\"]" }, + { "item": "minecraft:arrow[minecraft:rarity=epic]", amount: 64 } + ] + ``` + """ display_name = "Starting Items" @@ -109,22 +123,21 @@ def can_connect(cls, entrance, exit): return True -minecraft_options: typing.Dict[str, type(Option)] = { - "plando_connections": MCPlandoConnections, - "advancement_goal": AdvancementGoal, - "egg_shards_required": EggShardsRequired, - "egg_shards_available": EggShardsAvailable, - "required_bosses": BossGoal, - - "shuffle_structures": ShuffleStructures, - "structure_compasses": StructureCompasses, - - "combat_difficulty": CombatDifficulty, - "include_hard_advancements": HardAdvancements, - "include_unreasonable_advancements": UnreasonableAdvancements, - "include_postgame_advancements": PostgameAdvancements, - "bee_traps": BeeTraps, - "send_defeated_mobs": SendDefeatedMobs, - "death_link": DeathLink, - "starting_items": StartingItems, -} +@dataclass +class MinecraftOptions(PerGameCommonOptions): + plando_connections: MCPlandoConnections + advancement_goal: AdvancementGoal + egg_shards_required: EggShardsRequired + egg_shards_available: EggShardsAvailable + required_bosses: BossGoal + shuffle_structures: ShuffleStructures + structure_compasses: StructureCompasses + + combat_difficulty: CombatDifficulty + include_hard_advancements: HardAdvancements + include_unreasonable_advancements: UnreasonableAdvancements + include_postgame_advancements: PostgameAdvancements + bee_traps: BeeTraps + send_defeated_mobs: SendDefeatedMobs + death_link: DeathLink + starting_items: StartingItems diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index dae4241b992c..9a7be09a4a84 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -1,276 +1,471 @@ -import typing -from collections.abc import Callable - from BaseClasses import CollectionState from worlds.generic.Rules import exclusion_rules -from worlds.AutoWorld import World from . import Constants +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import MinecraftWorld + # Helper functions # moved from logicmixin -def has_iron_ingots(state: CollectionState, player: int) -> bool: +def has_iron_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) -def has_copper_ingots(state: CollectionState, player: int) -> bool: + +def has_copper_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) -def has_gold_ingots(state: CollectionState, player: int) -> bool: - return state.has('Progressive Resource Crafting', player) and (state.has('Progressive Tools', player, 2) or state.can_reach('The Nether', 'Region', player)) -def has_diamond_pickaxe(state: CollectionState, player: int) -> bool: - return state.has('Progressive Tools', player, 3) and has_iron_ingots(state, player) +def has_gold_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (state.has('Progressive Resource Crafting', player) + and ( + state.has('Progressive Tools', player, 2) + or state.can_reach_region('The Nether', player) + ) + ) + -def craft_crossbow(state: CollectionState, player: int) -> bool: - return state.has('Archery', player) and has_iron_ingots(state, player) +def has_diamond_pickaxe(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player, 3) and has_iron_ingots(world, state, player) -def has_bottle(state: CollectionState, player: int) -> bool: + +def craft_crossbow(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Archery', player) and has_iron_ingots(world, state, player) + + +def has_bottle(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player) -def has_spyglass(state: CollectionState, player: int) -> bool: - return has_copper_ingots(state, player) and state.has('Spyglass', player) and can_adventure(state, player) -def can_enchant(state: CollectionState, player: int) -> bool: - return state.has('Enchanting', player) and has_diamond_pickaxe(state, player) # mine obsidian and lapis +def has_spyglass(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (has_copper_ingots(world, state, player) + and state.has('Spyglass', player) + and can_adventure(world, state, player) + ) + + +def can_enchant(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and has_diamond_pickaxe(world, state, player) # mine obsidian and lapis -def can_use_anvil(state: CollectionState, player: int) -> bool: - return state.has('Enchanting', player) and state.has('Progressive Resource Crafting', player, 2) and has_iron_ingots(state, player) -def fortress_loot(state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls - return state.can_reach('Nether Fortress', 'Region', player) and basic_combat(state, player) +def can_use_anvil(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (state.has('Enchanting', player) + and state.has('Progressive Resource Crafting', player,2) + and has_iron_ingots(world, state, player) + ) -def can_brew_potions(state: CollectionState, player: int) -> bool: - return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(state, player) -def can_piglin_trade(state: CollectionState, player: int) -> bool: - return has_gold_ingots(state, player) and ( - state.can_reach('The Nether', 'Region', player) or - state.can_reach('Bastion Remnant', 'Region', player)) +def fortress_loot(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls + return state.can_reach_region('Nether Fortress', player) and basic_combat(world, state, player) -def overworld_villager(state: CollectionState, player: int) -> bool: + +def can_brew_potions(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(world, state, player) + + +def can_piglin_trade(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (has_gold_ingots(world, state, player) + and ( + state.can_reach_region('The Nether', player) + or state.can_reach_region('Bastion Remnant', player) + )) + + +def overworld_villager(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name - if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village - return (state.can_reach('Zombie Doctor', 'Location', player) or - (has_diamond_pickaxe(state, player) and state.can_reach('Village', 'Region', player))) + if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village + return (state.can_reach_location('Zombie Doctor', player) + or ( + has_diamond_pickaxe(world, state, player) + and state.can_reach_region('Village', player) + )) elif village_region == 'The End': - return state.can_reach('Zombie Doctor', 'Location', player) - return state.can_reach('Village', 'Region', player) + return state.can_reach_location('Zombie Doctor', player) + return state.can_reach_region('Village', player) -def enter_stronghold(state: CollectionState, player: int) -> bool: + +def enter_stronghold(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player) + # Difficulty-dependent functions -def combat_difficulty(state: CollectionState, player: int) -> bool: - return state.multiworld.combat_difficulty[player].current_key - -def can_adventure(state: CollectionState, player: int) -> bool: - death_link_check = not state.multiworld.death_link[player] or state.has('Bed', player) - if combat_difficulty(state, player) == 'easy': - return state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and death_link_check - elif combat_difficulty(state, player) == 'hard': +def combat_difficulty(world: "MinecraftWorld", state: CollectionState, player: int) -> str: + return world.options.combat_difficulty.current_key + + +def can_adventure(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + death_link_check = not world.options.death_link or state.has('Bed', player) + if combat_difficulty(world, state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and has_iron_ingots(world, state, player) and death_link_check + elif combat_difficulty(world, state, player) == 'hard': return True - return (state.has('Progressive Weapons', player) and death_link_check and - (state.has('Progressive Resource Crafting', player) or state.has('Campfire', player))) - -def basic_combat(state: CollectionState, player: int) -> bool: - if combat_difficulty(state, player) == 'easy': - return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and \ - state.has('Shield', player) and has_iron_ingots(state, player) - elif combat_difficulty(state, player) == 'hard': + return (state.has('Progressive Weapons', player) and death_link_check and + (state.has('Progressive Resource Crafting', player) or state.has('Campfire', player))) + + +def basic_combat(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + if combat_difficulty(world, state, player) == 'easy': + return (state.has('Progressive Weapons', player, 2) + and state.has('Progressive Armor', player) + and state.has('Shield', player) + and has_iron_ingots(world, state, player) + ) + elif combat_difficulty(world, state, player) == 'hard': return True - return state.has('Progressive Weapons', player) and (state.has('Progressive Armor', player) or state.has('Shield', player)) and has_iron_ingots(state, player) - -def complete_raid(state: CollectionState, player: int) -> bool: - reach_regions = state.can_reach('Village', 'Region', player) and state.can_reach('Pillager Outpost', 'Region', player) - if combat_difficulty(state, player) == 'easy': - return reach_regions and \ - state.has('Progressive Weapons', player, 3) and state.has('Progressive Armor', player, 2) and \ - state.has('Shield', player) and state.has('Archery', player) and \ - state.has('Progressive Tools', player, 2) and has_iron_ingots(state, player) - elif combat_difficulty(state, player) == 'hard': # might be too hard? - return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ - (state.has('Progressive Armor', player) or state.has('Shield', player)) - return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ - state.has('Progressive Armor', player) and state.has('Shield', player) - -def can_kill_wither(state: CollectionState, player: int) -> bool: - normal_kill = state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and can_brew_potions(state, player) and can_enchant(state, player) - if combat_difficulty(state, player) == 'easy': - return fortress_loot(state, player) and normal_kill and state.has('Archery', player) - elif combat_difficulty(state, player) == 'hard': # cheese kill using bedrock ceilings - return fortress_loot(state, player) and (normal_kill or state.can_reach('The Nether', 'Region', player) or state.can_reach('The End', 'Region', player)) - return fortress_loot(state, player) and normal_kill - -def can_respawn_ender_dragon(state: CollectionState, player: int) -> bool: - return state.can_reach('The Nether', 'Region', player) and state.can_reach('The End', 'Region', player) and \ - state.has('Progressive Resource Crafting', player) # smelt sand into glass - -def can_kill_ender_dragon(state: CollectionState, player: int) -> bool: - if combat_difficulty(state, player) == 'easy': - return state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and \ - state.has('Archery', player) and can_brew_potions(state, player) and can_enchant(state, player) - if combat_difficulty(state, player) == 'hard': - return (state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player)) or \ - (state.has('Progressive Weapons', player, 1) and state.has('Bed', player)) - return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and state.has('Archery', player) - -def has_structure_compass(state: CollectionState, entrance_name: str, player: int) -> bool: - if not state.multiworld.structure_compasses[player]: + return (state.has('Progressive Weapons', player) + and ( + state.has('Progressive Armor', player) + or state.has('Shield', player) + ) + and has_iron_ingots(world, state, player) + ) + + +def complete_raid(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + reach_regions = (state.can_reach_region('Village', player) + and state.can_reach_region('Pillager Outpost', player)) + if combat_difficulty(world, state, player) == 'easy': + return (reach_regions + and state.has('Progressive Weapons', player, 3) + and state.has('Progressive Armor', player, 2) + and state.has('Shield', player) + and state.has('Archery', player) + and state.has('Progressive Tools', player, 2) + and has_iron_ingots(world, state, player) + ) + elif combat_difficulty(world, state, player) == 'hard': # might be too hard? + return (reach_regions + and state.has('Progressive Weapons', player, 2) + and has_iron_ingots(world, state, player) + and ( + state.has('Progressive Armor', player) + or state.has('Shield', player) + ) + ) + return (reach_regions + and state.has('Progressive Weapons', player, 2) + and has_iron_ingots(world, state, player) + and state.has('Progressive Armor', player) + and state.has('Shield', player) + ) + + +def can_kill_wither(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + normal_kill = (state.has("Progressive Weapons", player, 3) + and state.has("Progressive Armor", player, 2) + and can_brew_potions(world, state, player) + and can_enchant(world, state, player) + ) + if combat_difficulty(world, state, player) == 'easy': + return (fortress_loot(world, state, player) + and normal_kill + and state.has('Archery', player) + ) + elif combat_difficulty(world, state, player) == 'hard': # cheese kill using bedrock ceilings + return (fortress_loot(world, state, player) + and ( + normal_kill + or state.can_reach_region('The Nether', player) + or state.can_reach_region('The End', player) + ) + ) + + return fortress_loot(world, state, player) and normal_kill + + +def can_respawn_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + return (state.can_reach_region('The Nether', player) + and state.can_reach_region('The End', player) + and state.has('Progressive Resource Crafting', player) # smelt sand into glass + ) + + +def can_kill_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: + if combat_difficulty(world, state, player) == 'easy': + return (state.has("Progressive Weapons", player, 3) + and state.has("Progressive Armor", player, 2) + and state.has('Archery', player) + and can_brew_potions(world, state, player) + and can_enchant(world, state, player) + ) + if combat_difficulty(world, state, player) == 'hard': + return ( + ( + state.has('Progressive Weapons', player, 2) + and state.has('Progressive Armor', player) + ) or ( + state.has('Progressive Weapons', player, 1) + and state.has('Bed', player) # who needs armor when you can respawn right outside the chamber + ) + ) + return (state.has('Progressive Weapons', player, 2) + and state.has('Progressive Armor', player) + and state.has('Archery', player) + ) + + +def has_structure_compass(world: "MinecraftWorld", state: CollectionState, entrance_name: str, player: int) -> bool: + if not world.options.structure_compasses: return True return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) -def get_rules_lookup(player: int): - rules_lookup: typing.Dict[str, typing.List[Callable[[CollectionState], bool]]] = { +def get_rules_lookup(world, player: int): + rules_lookup = { "entrances": { - "Nether Portal": lambda state: (state.has('Flint and Steel', player) and - (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and - has_iron_ingots(state, player)), - "End Portal": lambda state: enter_stronghold(state, player) and state.has('3 Ender Pearls', player, 4), - "Overworld Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 1", player)), - "Overworld Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 2", player)), - "Nether Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 1", player)), - "Nether Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 2", player)), - "The End Structure": lambda state: (can_adventure(state, player) and has_structure_compass(state, "The End Structure", player)), + "Nether Portal": lambda state: state.has('Flint and Steel', player) + and ( + state.has('Bucket', player) + or state.has('Progressive Tools', player, 3) + ) + and has_iron_ingots(world, state, player), + "End Portal": lambda state: enter_stronghold(world, state, player) + and state.has('3 Ender Pearls', player, 4), + "Overworld Structure 1": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Overworld Structure 1", player), + "Overworld Structure 2": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Overworld Structure 2", player), + "Nether Structure 1": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Nether Structure 1", player), + "Nether Structure 2": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "Nether Structure 2", player), + "The End Structure": lambda state: can_adventure(world, state, player) + and has_structure_compass(world, state, "The End Structure", player), }, "locations": { - "Ender Dragon": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), - "Wither": lambda state: can_kill_wither(state, player), - "Blaze Rods": lambda state: fortress_loot(state, player), - - "Who is Cutting Onions?": lambda state: can_piglin_trade(state, player), - "Oh Shiny": lambda state: can_piglin_trade(state, player), - "Suit Up": lambda state: state.has("Progressive Armor", player) and has_iron_ingots(state, player), - "Very Very Frightening": lambda state: (state.has("Channeling Book", player) and - can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), - "Hot Stuff": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), - "Free the End": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), - "A Furious Cocktail": lambda state: (can_brew_potions(state, player) and - state.has("Fishing Rod", player) and # Water Breathing - state.can_reach("The Nether", "Region", player) and # Regeneration, Fire Resistance, gold nuggets - state.can_reach("Village", "Region", player) and # Night Vision, Invisibility - state.can_reach("Bring Home the Beacon", "Location", player)), # Resistance - "Bring Home the Beacon": lambda state: (can_kill_wither(state, player) and - has_diamond_pickaxe(state, player) and state.has("Progressive Resource Crafting", player, 2)), - "Not Today, Thank You": lambda state: state.has("Shield", player) and has_iron_ingots(state, player), - "Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), - "Local Brewery": lambda state: can_brew_potions(state, player), - "The Next Generation": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Ender Dragon": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), + "Wither": lambda state: can_kill_wither(world, state, player), + "Blaze Rods": lambda state: fortress_loot(world, state, player), + "Who is Cutting Onions?": lambda state: can_piglin_trade(world, state, player), + "Oh Shiny": lambda state: can_piglin_trade(world, state, player), + "Suit Up": lambda state: state.has("Progressive Armor", player) + and has_iron_ingots(world, state, player), + "Very Very Frightening": lambda state: state.has("Channeling Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player) + and overworld_villager(world, state, player), + "Hot Stuff": lambda state: state.has("Bucket", player) + and has_iron_ingots(world, state, player), + "Free the End": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), + "A Furious Cocktail": lambda state: (can_brew_potions(world, state, player) + and state.has("Fishing Rod", player) # Water Breathing + and state.can_reach_region("The Nether", player) # Regeneration, Fire Resistance, gold nuggets + and state.can_reach_region("Village", player) # Night Vision, Invisibility + and state.can_reach_location("Bring Home the Beacon", player)), + # Resistance + "Bring Home the Beacon": lambda state: can_kill_wither(world, state, player) + and has_diamond_pickaxe(world, state, player) + and state.has("Progressive Resource Crafting", player, 2), + "Not Today, Thank You": lambda state: state.has("Shield", player) + and has_iron_ingots(world, state, player), + "Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Local Brewery": lambda state: can_brew_potions(world, state, player), + "The Next Generation": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), "Fishy Business": lambda state: state.has("Fishing Rod", player), - "This Boat Has Legs": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player)), + "This Boat Has Legs": lambda state: ( + fortress_loot(world, state, player) + or complete_raid(world, state, player) + ) + and state.has("Saddle", player) + and state.has("Fishing Rod", player), "Sniper Duel": lambda state: state.has("Archery", player), - "Great View From Up Here": lambda state: basic_combat(state, player), - "How Did We Get Here?": lambda state: (can_brew_potions(state, player) and - has_gold_ingots(state, player) and # Absorption - state.can_reach('End City', 'Region', player) and # Levitation - state.can_reach('The Nether', 'Region', player) and # potion ingredients - state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows - state.can_reach("Bring Home the Beacon", "Location", player) and # Haste - state.can_reach("Hero of the Village", "Location", player)), # Bad Omen, Hero of the Village - "Bullseye": lambda state: (state.has("Archery", player) and state.has("Progressive Tools", player, 2) and - has_iron_ingots(state, player)), - "Spooky Scary Skeleton": lambda state: basic_combat(state, player), - "Two by Two": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player) and can_adventure(state, player), - "Two Birds, One Arrow": lambda state: craft_crossbow(state, player) and can_enchant(state, player), - "Who's the Pillager Now?": lambda state: craft_crossbow(state, player), + "Great View From Up Here": lambda state: basic_combat(world, state, player), + "How Did We Get Here?": lambda state: (can_brew_potions(world, state, player) + and has_gold_ingots(world, state, player) # Absorption + and state.can_reach_region('End City', player) # Levitation + and state.can_reach_region('The Nether', player) # potion ingredients + and state.has("Fishing Rod", player) # Pufferfish, Nautilus Shells; spectral arrows + and state.has("Archery", player) + and state.can_reach_location("Bring Home the Beacon", player) # Haste + and state.can_reach_location("Hero of the Village", player)), # Bad Omen, Hero of the Village + "Bullseye": lambda state: state.has("Archery", player) + and state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Spooky Scary Skeleton": lambda state: basic_combat(world, state, player), + "Two by Two": lambda state: has_iron_ingots(world, state, player) + and state.has("Bucket", player) + and can_adventure(world, state, player), + "Two Birds, One Arrow": lambda state: craft_crossbow(world, state, player) + and can_enchant(world, state, player), + "Who's the Pillager Now?": lambda state: craft_crossbow(world, state, player), "Getting an Upgrade": lambda state: state.has("Progressive Tools", player), - "Tactical Fishing": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), - "Zombie Doctor": lambda state: can_brew_potions(state, player) and has_gold_ingots(state, player), - "Ice Bucket Challenge": lambda state: has_diamond_pickaxe(state, player), - "Into Fire": lambda state: basic_combat(state, player), - "War Pigs": lambda state: basic_combat(state, player), + "Tactical Fishing": lambda state: state.has("Bucket", player) + and has_iron_ingots(world, state, player), + "Zombie Doctor": lambda state: can_brew_potions(world, state, player) + and has_gold_ingots(world, state, player), + "Ice Bucket Challenge": lambda state: has_diamond_pickaxe(world, state, player), + "Into Fire": lambda state: basic_combat(world, state, player), + "War Pigs": lambda state: basic_combat(world, state, player), "Take Aim": lambda state: state.has("Archery", player), - "Total Beelocation": lambda state: state.has("Silk Touch Book", player) and can_use_anvil(state, player) and can_enchant(state, player), - "Arbalistic": lambda state: (craft_crossbow(state, player) and state.has("Piercing IV Book", player) and - can_use_anvil(state, player) and can_enchant(state, player)), - "The End... Again...": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), - "Acquire Hardware": lambda state: has_iron_ingots(state, player), - "Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(state, player) and state.has("Progressive Resource Crafting", player, 2), - "Cover Me With Diamonds": lambda state: (state.has("Progressive Armor", player, 2) and - state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player)), - "Sky's the Limit": lambda state: basic_combat(state, player), - "Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) and has_iron_ingots(state, player), - "Sweet Dreams": lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player), - "You Need a Mint": lambda state: can_respawn_ender_dragon(state, player) and has_bottle(state, player), - "Monsters Hunted": lambda state: (can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player) and - can_kill_wither(state, player) and state.has("Fishing Rod", player)), - "Enchanter": lambda state: can_enchant(state, player), - "Voluntary Exile": lambda state: basic_combat(state, player), - "Eye Spy": lambda state: enter_stronghold(state, player), - "Serious Dedication": lambda state: (can_brew_potions(state, player) and state.has("Bed", player) and - has_diamond_pickaxe(state, player) and has_gold_ingots(state, player)), - "Postmortal": lambda state: complete_raid(state, player), - "Adventuring Time": lambda state: can_adventure(state, player), - "Hero of the Village": lambda state: complete_raid(state, player), - "Hidden in the Depths": lambda state: can_brew_potions(state, player) and state.has("Bed", player) and has_diamond_pickaxe(state, player), - "Beaconator": lambda state: (can_kill_wither(state, player) and has_diamond_pickaxe(state, player) and - state.has("Progressive Resource Crafting", player, 2)), - "Withering Heights": lambda state: can_kill_wither(state, player), - "A Balanced Diet": lambda state: (has_bottle(state, player) and has_gold_ingots(state, player) and # honey bottle; gapple - state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)), # notch apple, chorus fruit - "Subspace Bubble": lambda state: has_diamond_pickaxe(state, player), - "Country Lode, Take Me Home": lambda state: state.can_reach("Hidden in the Depths", "Location", player) and has_gold_ingots(state, player), - "Bee Our Guest": lambda state: state.has("Campfire", player) and has_bottle(state, player), - "Uneasy Alliance": lambda state: has_diamond_pickaxe(state, player) and state.has('Fishing Rod', player), - "Diamonds!": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), - "A Throwaway Joke": lambda state: can_adventure(state, player), - "Sticky Situation": lambda state: state.has("Campfire", player) and has_bottle(state, player), - "Ol' Betsy": lambda state: craft_crossbow(state, player), - "Cover Me in Debris": lambda state: (state.has("Progressive Armor", player, 2) and - state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and - has_diamond_pickaxe(state, player) and has_iron_ingots(state, player) and - can_brew_potions(state, player) and state.has("Bed", player)), + "Total Beelocation": lambda state: state.has("Silk Touch Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player), + "Arbalistic": lambda state: (craft_crossbow(world, state, player) + and state.has("Piercing IV Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player) + ), + "The End... Again...": lambda state: can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player), + "Acquire Hardware": lambda state: has_iron_ingots(world, state, player), + "Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(world, state, player) + and state.has("Progressive Resource Crafting", player, 2), + "Cover Me With Diamonds": lambda state: state.has("Progressive Armor", player, 2) + and state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Sky's the Limit": lambda state: basic_combat(world, state, player), + "Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) + and has_iron_ingots(world, state, player), + "Sweet Dreams": lambda state: state.has("Bed", player) + or state.can_reach_region('Village', player), + "You Need a Mint": lambda state: can_respawn_ender_dragon(world, state, player) + and has_bottle(world, state, player), + "Monsters Hunted": lambda state: (can_respawn_ender_dragon(world, state, player) + and can_kill_ender_dragon(world, state, player) + and can_kill_wither(world, state, player) + and state.has("Fishing Rod", player)), + "Enchanter": lambda state: can_enchant(world, state, player), + "Voluntary Exile": lambda state: basic_combat(world, state, player), + "Eye Spy": lambda state: enter_stronghold(world, state, player), + "Serious Dedication": lambda state: (can_brew_potions(world, state, player) + and state.has("Bed", player) + and has_diamond_pickaxe(world, state, player) + and has_gold_ingots(world, state, player)), + "Postmortal": lambda state: complete_raid(world, state, player), + "Adventuring Time": lambda state: can_adventure(world, state, player), + "Hero of the Village": lambda state: complete_raid(world, state, player), + "Hidden in the Depths": lambda state: can_brew_potions(world, state, player) + and state.has("Bed", player) + and has_diamond_pickaxe(world, state, player), + "Beaconator": lambda state: (can_kill_wither(world, state, player) + and has_diamond_pickaxe(world, state, player) + and state.has("Progressive Resource Crafting", player, 2)), + "Withering Heights": lambda state: can_kill_wither(world, state, player), + "A Balanced Diet": lambda state: (has_bottle(world, state, player) + and has_gold_ingots(world, state, player) + and state.has("Progressive Resource Crafting", player, 2) + and state.can_reach_region('The End', player)), + # notch apple, chorus fruit + "Subspace Bubble": lambda state: has_diamond_pickaxe(world, state, player), + "Country Lode, Take Me Home": lambda state: state.can_reach_location("Hidden in the Depths", player) + and has_gold_ingots(world, state, player), + "Bee Our Guest": lambda state: state.has("Campfire", player) + and has_bottle(world, state, player), + "Uneasy Alliance": lambda state: has_diamond_pickaxe(world, state, player) + and state.has('Fishing Rod', player), + "Diamonds!": lambda state: state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "A Throwaway Joke": lambda state: can_adventure(world, state, player), + "Sticky Situation": lambda state: state.has("Campfire", player) + and has_bottle(world, state, player), + "Ol' Betsy": lambda state: craft_crossbow(world, state, player), + "Cover Me in Debris": lambda state: state.has("Progressive Armor", player, 2) + and state.has("8 Netherite Scrap", player, 2) + and state.has("Progressive Resource Crafting", player) + and has_diamond_pickaxe(world, state, player) + and has_iron_ingots(world, state, player) + and can_brew_potions(world, state, player) + and state.has("Bed", player), "Hot Topic": lambda state: state.has("Progressive Resource Crafting", player), - "The Lie": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player), - "On a Rail": lambda state: has_iron_ingots(state, player) and state.has('Progressive Tools', player, 2), - "When Pigs Fly": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player) and can_adventure(state, player)), - "Overkill": lambda state: (can_brew_potions(state, player) and - (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))), + "The Lie": lambda state: has_iron_ingots(world, state, player) + and state.has("Bucket", player), + "On a Rail": lambda state: has_iron_ingots(world, state, player) + and state.has('Progressive Tools', player, 2), + "When Pigs Fly": lambda state: ( + fortress_loot(world, state, player) + or complete_raid(world, state, player) + ) + and state.has("Saddle", player) + and state.has("Fishing Rod", player) + and can_adventure(world, state, player), + "Overkill": lambda state: can_brew_potions(world, state, player) + and ( + state.has("Progressive Weapons", player) + or state.can_reach_region('The Nether', player) + ), "Librarian": lambda state: state.has("Enchanting", player), - "Overpowered": lambda state: (has_iron_ingots(state, player) and - state.has('Progressive Tools', player, 2) and basic_combat(state, player)), - "Wax On": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)), - "Wax Off": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)), - "The Cutest Predator": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), - "The Healing Power of Friendship": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), - "Is It a Bird?": lambda state: has_spyglass(state, player) and can_adventure(state, player), - "Is It a Balloon?": lambda state: has_spyglass(state, player), - "Is It a Plane?": lambda state: has_spyglass(state, player) and can_respawn_ender_dragon(state, player), - "Surge Protector": lambda state: (state.has("Channeling Book", player) and - can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), - "Light as a Rabbit": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has('Bucket', player), - "Glow and Behold!": lambda state: can_adventure(state, player), - "Whatever Floats Your Goat!": lambda state: can_adventure(state, player), - "Caves & Cliffs": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2), - "Feels like home": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and - (fortress_loot(state, player) or complete_raid(state, player)) and state.has("Saddle", player)), - "Sound of Music": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player) and basic_combat(state, player), - "Star Trader": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and - (state.can_reach("The Nether", 'Region', player) or - state.can_reach("Nether Fortress", 'Region', player) or # soul sand for water elevator - can_piglin_trade(state, player)) and - overworld_villager(state, player)), - "Birthday Song": lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), - "Bukkit Bukkit": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player) and can_adventure(state, player), - "It Spreads": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), - "Sneak 100": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), - "When the Squad Hops into Town": lambda state: can_adventure(state, player) and state.has("Lead", player), - "With Our Powers Combined!": lambda state: can_adventure(state, player) and state.has("Lead", player), + "Overpowered": lambda state: has_iron_ingots(world, state, player) + and state.has('Progressive Tools', player, 2) + and basic_combat(world, state, player), + "Wax On": lambda state: has_copper_ingots(world, state, player) + and state.has('Campfire', player) + and state.has('Progressive Resource Crafting', player, 2), + "Wax Off": lambda state: has_copper_ingots(world, state, player) + and state.has('Campfire', player) + and state.has('Progressive Resource Crafting', player, 2), + "The Cutest Predator": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player), + "The Healing Power of Friendship": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player), + "Is It a Bird?": lambda state: has_spyglass(world, state, player) + and can_adventure(world, state, player), + "Is It a Balloon?": lambda state: has_spyglass(world, state, player), + "Is It a Plane?": lambda state: has_spyglass(world, state, player) + and can_respawn_ender_dragon(world, state, player), + "Surge Protector": lambda state: state.has("Channeling Book", player) + and can_use_anvil(world, state, player) + and can_enchant(world, state, player) + and overworld_villager(world, state, player), + "Light as a Rabbit": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has('Bucket', player), + "Glow and Behold!": lambda state: can_adventure(world, state, player), + "Whatever Floats Your Goat!": lambda state: can_adventure(world, state, player), + "Caves & Cliffs": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player) + and state.has('Progressive Tools', player, 2), + "Feels like home": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player) + and state.has('Fishing Rod', player) + and ( + fortress_loot(world, state, player) + or complete_raid(world, state, player) + ) + and state.has("Saddle", player), + "Sound of Music": lambda state: state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player) + and basic_combat(world, state, player), + "Star Trader": lambda state: has_iron_ingots(world, state, player) + and state.has('Bucket', player) + and ( + state.can_reach_region("The Nether", player) # soul sand in nether + or state.can_reach_region("Nether Fortress", player) # soul sand in fortress if not in nether for water elevator + or can_piglin_trade(world, state, player) # piglins give soul sand + ) + and overworld_villager(world, state, player), + "Birthday Song": lambda state: state.can_reach_location("The Lie", player) + and state.has("Progressive Tools", player, 2) + and has_iron_ingots(world, state, player), + "Bukkit Bukkit": lambda state: state.has("Bucket", player) + and has_iron_ingots(world, state, player) + and can_adventure(world, state, player), + "It Spreads": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has("Progressive Tools", player, 2), + "Sneak 100": lambda state: can_adventure(world, state, player) + and has_iron_ingots(world, state, player) + and state.has("Progressive Tools", player, 2), + "When the Squad Hops into Town": lambda state: can_adventure(world, state, player) + and state.has("Lead", player), + "With Our Powers Combined!": lambda state: can_adventure(world, state, player) + and state.has("Lead", player), } } return rules_lookup -def set_rules(mc_world: World) -> None: - multiworld = mc_world.multiworld - player = mc_world.player +def set_rules(self: "MinecraftWorld") -> None: + multiworld = self.multiworld + player = self.player - rules_lookup = get_rules_lookup(player) + rules_lookup = get_rules_lookup(self, player) # Set entrance rules for entrance_name, rule in rules_lookup["entrances"].items(): @@ -281,33 +476,33 @@ def set_rules(mc_world: World) -> None: multiworld.get_location(location_name, player).access_rule = rule # Set rules surrounding completion - bosses = multiworld.required_bosses[player] + bosses = self.options.required_bosses postgame_advancements = set() if bosses.dragon: postgame_advancements.update(Constants.exclusion_info["ender_dragon"]) if bosses.wither: postgame_advancements.update(Constants.exclusion_info["wither"]) - def location_count(state: CollectionState) -> bool: + def location_count(state: CollectionState) -> int: return len([location for location in multiworld.get_locations(player) if - location.address != None and - location.can_reach(state)]) + location.address is not None and + location.can_reach(state)]) def defeated_bosses(state: CollectionState) -> bool: return ((not bosses.dragon or state.has("Ender Dragon", player)) - and (not bosses.wither or state.has("Wither", player))) + and (not bosses.wither or state.has("Wither", player))) - egg_shards = min(multiworld.egg_shards_required[player], multiworld.egg_shards_available[player]) - completion_requirements = lambda state: (location_count(state) >= multiworld.advancement_goal[player] - and state.has("Dragon Egg Shard", player, egg_shards)) + egg_shards = min(self.options.egg_shards_required.value, self.options.egg_shards_available.value) + completion_requirements = lambda state: (location_count(state) >= self.options.advancement_goal + and state.has("Dragon Egg Shard", player, egg_shards)) multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state) # Set exclusions on hard/unreasonable/postgame excluded_advancements = set() - if not multiworld.include_hard_advancements[player]: + if not self.options.include_hard_advancements: excluded_advancements.update(Constants.exclusion_info["hard"]) - if not multiworld.include_unreasonable_advancements[player]: + if not self.options.include_unreasonable_advancements: excluded_advancements.update(Constants.exclusion_info["unreasonable"]) - if not multiworld.include_postgame_advancements[player]: + if not self.options.include_postgame_advancements: excluded_advancements.update(postgame_advancements) exclusion_rules(multiworld, player, excluded_advancements) diff --git a/worlds/minecraft/Structures.py b/worlds/minecraft/Structures.py index 95bafc9efb5c..d4f62f3498e9 100644 --- a/worlds/minecraft/Structures.py +++ b/worlds/minecraft/Structures.py @@ -1,17 +1,19 @@ -from worlds.AutoWorld import World - from . import Constants +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import MinecraftWorld + -def shuffle_structures(mc_world: World) -> None: - multiworld = mc_world.multiworld - player = mc_world.player +def shuffle_structures(self: "MinecraftWorld") -> None: + multiworld = self.multiworld + player = self.player default_connections = Constants.region_info["default_connections"] illegal_connections = Constants.region_info["illegal_connections"] # Get all unpaired exits and all regions without entrances (except the Menu) # This function is destructive on these lists. - exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region == None] + exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region is None] structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] exits_spoiler = exits[:] # copy the original order for the spoiler log @@ -26,19 +28,19 @@ def set_pair(exit, struct): raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})") # Connect plando structures first - if multiworld.plando_connections[player]: - for conn in multiworld.plando_connections[player]: + if self.options.plando_connections: + for conn in self.options.plando_connections: set_pair(conn.entrance, conn.exit) # The algorithm tries to place the most restrictive structures first. This algorithm always works on the # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. - if multiworld.shuffle_structures[player]: + if self.options.shuffle_structures: structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) for struct in structs[:]: try: - exit = multiworld.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) + exit = self.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) except IndexError: - raise Exception(f"No valid structure placements remaining for player {player} ({multiworld.player_name[player]})") + raise Exception(f"No valid structure placements remaining for player {player} ({self.player_name})") set_pair(exit, struct) else: # write remaining default connections for (exit, struct) in default_connections: @@ -49,9 +51,9 @@ def set_pair(exit, struct): try: assert len(exits) == len(structs) == 0 except AssertionError: - raise Exception(f"Failed to connect all Minecraft structures for player {player} ({multiworld.player_name[player]})") + raise Exception(f"Failed to connect all Minecraft structures for player {player} ({self.player_name})") for exit in exits_spoiler: multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player)) - if multiworld.shuffle_structures[player] or multiworld.plando_connections[player]: + if self.options.shuffle_structures or self.options.plando_connections: multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 75e043d0cbaf..75539fcf2ea6 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -9,7 +9,7 @@ from worlds.AutoWorld import World, WebWorld from . import Constants -from .Options import minecraft_options +from .Options import MinecraftOptions from .Structures import shuffle_structures from .ItemPool import build_item_pool, get_junk_item_names from .Rules import set_rules @@ -83,8 +83,9 @@ class MinecraftWorld(World): structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim victory! """ - game: str = "Minecraft" - option_definitions = minecraft_options + game = "Minecraft" + options_dataclass = MinecraftOptions + options: MinecraftOptions settings: typing.ClassVar[MinecraftSettings] topology_present = True web = MinecraftWebWorld() @@ -95,20 +96,20 @@ class MinecraftWorld(World): def _get_mc_data(self) -> Dict[str, Any]: exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { - 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), + 'world_seed': self.random.getrandbits(32), 'seed_name': self.multiworld.seed_name, - 'player_name': self.multiworld.get_player_name(self.player), + 'player_name': self.player_name, 'player_id': self.player, 'client_version': client_version, 'structures': {exit: self.multiworld.get_entrance(exit, self.player).connected_region.name for exit in exits}, - 'advancement_goal': self.multiworld.advancement_goal[self.player].value, - 'egg_shards_required': min(self.multiworld.egg_shards_required[self.player].value, - self.multiworld.egg_shards_available[self.player].value), - 'egg_shards_available': self.multiworld.egg_shards_available[self.player].value, - 'required_bosses': self.multiworld.required_bosses[self.player].current_key, - 'MC35': bool(self.multiworld.send_defeated_mobs[self.player].value), - 'death_link': bool(self.multiworld.death_link[self.player].value), - 'starting_items': str(self.multiworld.starting_items[self.player].value), + 'advancement_goal': self.options.advancement_goal.value, + 'egg_shards_required': min(self.options.egg_shards_required.value, + self.options.egg_shards_available.value), + 'egg_shards_available': self.options.egg_shards_available.value, + 'required_bosses': self.options.required_bosses.current_key, + 'MC35': bool(self.options.send_defeated_mobs.value), + 'death_link': bool(self.options.death_link.value), + 'starting_items': json.dumps(self.options.starting_items.value), 'race': self.multiworld.is_race, } @@ -129,7 +130,7 @@ def create_event(self, region_name: str, event_name: str) -> None: loc.place_locked_item(self.create_event_item(event_name)) region.locations.append(loc) - def create_event_item(self, name: str) -> None: + def create_event_item(self, name: str) -> Item: item = self.create_item(name) item.classification = ItemClassification.progression return item @@ -176,15 +177,10 @@ def generate_output(self, output_directory: str) -> None: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) def fill_slot_data(self) -> dict: - slot_data = self._get_mc_data() - for option_name in minecraft_options: - option = getattr(self.multiworld, option_name)[self.player] - if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: - slot_data[option_name] = int(option.value) - return slot_data + return self._get_mc_data() def get_filler_item_name(self) -> str: - return get_junk_item_names(self.multiworld.random, 1)[0] + return get_junk_item_names(self.random, 1)[0] class MinecraftLocation(Location): diff --git a/worlds/minecraft/docs/minecraft_es.md b/worlds/minecraft/docs/minecraft_es.md index 3f2df6e7ba76..4f4899212240 100644 --- a/worlds/minecraft/docs/minecraft_es.md +++ b/worlds/minecraft/docs/minecraft_es.md @@ -29,7 +29,7 @@ name: TuNombre game: Minecraft # Opciones compartidas por todos los juegos: -accessibility: locations +accessibility: full progression_balancing: 50 # Opciones Especficicas para Minecraft diff --git a/worlds/minecraft/docs/minecraft_sv.md b/worlds/minecraft/docs/minecraft_sv.md index fd89d681ee37..ab8c1b5d8ea7 100644 --- a/worlds/minecraft/docs/minecraft_sv.md +++ b/worlds/minecraft/docs/minecraft_sv.md @@ -79,7 +79,7 @@ description: Template Name # Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns. name: YourName game: Minecraft -accessibility: locations +accessibility: full progression_balancing: 0 advancement_goal: few: 0 diff --git a/worlds/minecraft/test/TestOptions.py b/worlds/minecraft/test/TestOptions.py index 668ed500e832..c04a07054c9c 100644 --- a/worlds/minecraft/test/TestOptions.py +++ b/worlds/minecraft/test/TestOptions.py @@ -1,19 +1,19 @@ from . import MCTestBase from ..Constants import region_info -from ..Options import minecraft_options +from .. import Options from BaseClasses import ItemClassification class AdvancementTestBase(MCTestBase): options = { - "advancement_goal": minecraft_options["advancement_goal"].range_end + "advancement_goal": Options.AdvancementGoal.range_end } # beatability test implicit class ShardTestBase(MCTestBase): options = { - "egg_shards_required": minecraft_options["egg_shards_required"].range_end, - "egg_shards_available": minecraft_options["egg_shards_available"].range_end + "egg_shards_required": Options.EggShardsRequired.range_end, + "egg_shards_available": Options.EggShardsAvailable.range_end } # check that itempool is not overfilled with shards @@ -29,7 +29,7 @@ def test_compasses_in_pool(self): class NoBeeTestBase(MCTestBase): options = { - "bee_traps": 0 + "bee_traps": Options.BeeTraps.range_start } # With no bees, there are no traps in the pool @@ -40,7 +40,7 @@ def test_bees(self): class AllBeeTestBase(MCTestBase): options = { - "bee_traps": 100 + "bee_traps": Options.BeeTraps.range_end } # With max bees, there are no filler items, only bee traps diff --git a/worlds/minecraft/test/__init__.py b/worlds/minecraft/test/__init__.py index acf9b7949137..3d936fe9cb6b 100644 --- a/worlds/minecraft/test/__init__.py +++ b/worlds/minecraft/test/__init__.py @@ -1,5 +1,5 @@ -from test.TestBase import TestBase, WorldTestBase -from .. import MinecraftWorld +from test.bases import TestBase, WorldTestBase +from .. import MinecraftWorld, MinecraftOptions class MCTestBase(WorldTestBase, TestBase): 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 156d28f346e8..7ed6c38ea9f4 100644 Binary files a/worlds/mlss/data/basepatch.bsdiff and b/worlds/mlss/data/basepatch.bsdiff differ diff --git a/worlds/mm2/__init__.py b/worlds/mm2/__init__.py new file mode 100644 index 000000000000..4a43ee8df0f0 --- /dev/null +++ b/worlds/mm2/__init__.py @@ -0,0 +1,290 @@ +import hashlib +import logging +from copy import deepcopy +from typing import Dict, Any, TYPE_CHECKING, Optional, Sequence, Tuple, ClassVar, List + +from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location +from worlds.AutoWorld import World, WebWorld +from .names import (dr_wily, heat_man_stage, air_man_stage, wood_man_stage, bubble_man_stage, quick_man_stage, + flash_man_stage, metal_man_stage, crash_man_stage) +from .items import (item_table, item_names, MM2Item, filler_item_weights, robot_master_weapon_table, + stage_access_table, item_item_table, lookup_item_to_id) +from .locations import (MM2Location, mm2_regions, MM2Region, energy_pickups, etank_1ups, lookup_location_to_id, + location_groups) +from .rom import patch_rom, MM2ProcedurePatch, MM2LCHASH, PROTEUSHASH, MM2VCHASH, MM2NESHASH +from .options import MM2Options, Consumables +from .client import MegaMan2Client +from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement +import os +import threading +import base64 +import settings +logger = logging.getLogger("Mega Man 2") + +if TYPE_CHECKING: + from BaseClasses import CollectionState + + +class MM2Settings(settings.Group): + class RomFile(settings.UserFilePath): + """File name of the MM2 EN rom""" + description = "Mega Man 2 ROM File" + copy_to: Optional[str] = "Mega Man 2 (USA).nes" + md5s = [MM2NESHASH, MM2VCHASH, MM2LCHASH, PROTEUSHASH] + + def browse(self: settings.T, + filetypes: Optional[Sequence[Tuple[str, Sequence[str]]]] = None, + **kwargs: Any) -> Optional[settings.T]: + if not filetypes: + file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux + return super().browse(file_types, **kwargs) + else: + return super().browse(filetypes, **kwargs) + + @classmethod + def validate(cls, path: str) -> None: + """Try to open and validate file against hashes""" + with open(path, "rb", buffering=0) as f: + try: + f.seek(0) + if f.read(4) == b"NES\x1A": + f.seek(16) + else: + f.seek(0) + cls._validate_stream_hashes(f) + base_rom_bytes = f.read() + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() == PROTEUSHASH: + # we need special behavior here + cls.copy_to = None + except ValueError: + raise ValueError(f"File hash does not match for {path}") + + rom_file: RomFile = RomFile(RomFile.copy_to) + + +class MM2WebWorld(WebWorld): + theme = "partyTime" + tutorials = [ + + Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Mega Man 2 randomizer connected to an Archipelago Multiworld.", + "English", + "setup_en.md", + "setup/en", + ["Silvris"] + ) + ] + + +class MM2World(World): + """ + In the year 200X, following his prior defeat by Mega Man, the evil Dr. Wily has returned to take over the world with + his own group of Robot Masters. Mega Man once again sets out to defeat the eight Robot Masters and stop Dr. Wily. + + """ + + game = "Mega Man 2" + settings: ClassVar[MM2Settings] + options_dataclass = MM2Options + options: MM2Options + item_name_to_id = lookup_item_to_id + location_name_to_id = lookup_location_to_id + item_name_groups = item_names + location_name_groups = location_groups + web = MM2WebWorld() + rom_name: bytearray + world_version: Tuple[int, int, int] = (0, 3, 2) + wily_5_weapons: Dict[int, List[int]] + + def __init__(self, multiworld: MultiWorld, player: int): + self.rom_name = bytearray() + self.rom_name_available_event = threading.Event() + super().__init__(multiworld, player) + self.weapon_damage = deepcopy(weapon_damage) + self.wily_5_weapons = {} + + def create_regions(self) -> None: + menu = MM2Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu) + for region in mm2_regions: + stage = MM2Region(region, self.player, self.multiworld) + required_items = mm2_regions[region][0] + locations = mm2_regions[region][1] + prev_stage = mm2_regions[region][2] + if prev_stage is None: + menu.connect(stage, f"To {region}", + lambda state, items=required_items: state.has_all(items, self.player)) + else: + old_stage = self.get_region(prev_stage) + old_stage.connect(stage, f"To {region}", + lambda state, items=required_items: state.has_all(items, self.player)) + stage.add_locations(locations, MM2Location) + for location in stage.get_locations(): + if location.address is None and location.name != dr_wily: + location.place_locked_item(MM2Item(location.name, ItemClassification.progression, + None, self.player)) + if region in etank_1ups and self.options.consumables in (Consumables.option_1up_etank, + Consumables.option_all): + stage.add_locations(etank_1ups[region], MM2Location) + if region in energy_pickups and self.options.consumables in (Consumables.option_weapon_health, + Consumables.option_all): + stage.add_locations(energy_pickups[region], MM2Location) + self.multiworld.regions.append(stage) + + def create_item(self, name: str) -> MM2Item: + item = item_table[name] + classification = ItemClassification.filler + if item.progression: + classification = ItemClassification.progression_skip_balancing \ + if item.skip_balancing else ItemClassification.progression + if item.useful: + classification |= ItemClassification.useful + return MM2Item(name, classification, item.code, self.player) + + def get_filler_item_name(self) -> str: + return self.random.choices(list(filler_item_weights.keys()), + weights=list(filler_item_weights.values()))[0] + + def create_items(self) -> None: + itempool = [] + # grab first robot master + robot_master = self.item_id_to_name[0x880101 + self.options.starting_robot_master.value] + self.multiworld.push_precollected(self.create_item(robot_master)) + itempool.extend([self.create_item(name) for name in stage_access_table.keys() + if name != robot_master]) + itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()]) + itempool.extend([self.create_item(name) for name in item_item_table.keys()]) + total_checks = 24 + if self.options.consumables in (Consumables.option_1up_etank, + Consumables.option_all): + total_checks += 20 + if self.options.consumables in (Consumables.option_weapon_health, + Consumables.option_all): + total_checks += 27 + remaining = total_checks - len(itempool) + itempool.extend([self.create_item(name) + for name in self.random.choices(list(filler_item_weights.keys()), + weights=list(filler_item_weights.values()), + k=remaining)]) + self.multiworld.itempool += itempool + + set_rules = set_rules + + def generate_early(self) -> None: + if (not self.options.yoku_jumps + and self.options.starting_robot_master == "heat_man") or \ + (not self.options.enable_lasers + and self.options.starting_robot_master == "quick_man"): + robot_master_pool = [1, 2, 3, 5, 6, 7, ] + if self.options.yoku_jumps: + robot_master_pool.append(0) + if self.options.enable_lasers: + robot_master_pool.append(4) + self.options.starting_robot_master.value = self.random.choice(robot_master_pool) + logger.warning( + f"Mega Man 2 ({self.player_name}): " + f"Incompatible starting Robot Master, changing to " + f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}") + + def generate_basic(self) -> None: + goal_location = self.get_location(dr_wily) + goal_location.place_locked_item(MM2Item("Victory", ItemClassification.progression, None, self.player)) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + + def fill_hook(self, + progitempool: List["Item"], + usefulitempool: List["Item"], + filleritempool: List["Item"], + fill_locations: List["Location"]) -> None: + # on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible + # since MM2 can have a 2 item sphere 1, and 3 items are required for Wily + if self.multiworld.players > 1: + return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1 + rbm_to_item = { + 0: heat_man_stage, + 1: air_man_stage, + 2: wood_man_stage, + 3: bubble_man_stage, + 4: quick_man_stage, + 5: flash_man_stage, + 6: metal_man_stage, + 7: crash_man_stage + } + affected_rbm = [2, 3] # Wood and Bubble will always have this happen + possible_rbm = [1, 5] # Air and Flash are always valid targets, due to Item 2/3 receive + if self.options.consumables: + possible_rbm.append(6) # Metal has 3 consumables + possible_rbm.append(7) # Crash has 3 consumables + if self.options.enable_lasers: + possible_rbm.append(4) # Quick has a lot of consumables, but needs logical time stopper if not enabled + else: + affected_rbm.extend([6, 7]) # only two checks on non consumables + if self.options.yoku_jumps: + possible_rbm.append(0) # Heat has 3 locations always, but might need 2 items logically + if self.options.starting_robot_master.value in affected_rbm: + rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm)) + valid_second = [item for item in progitempool + if item.name in rbm_names + and item.player == self.player] + placed_item = self.random.choice(valid_second) + rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}" + f" - Defeated") + rbm_location = self.get_location(rbm_defeated) + rbm_location.place_locked_item(placed_item) + progitempool.remove(placed_item) + fill_locations.remove(rbm_location) + target_rbm = (placed_item.code & 0xF) - 1 + if self.options.strict_weakness or (self.options.random_weakness + and not (self.weapon_damage[0][target_rbm] > 0)): + # we need to find a weakness for this boss + weaknesses = [weapon for weapon in range(1, 9) + if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]] + weapons = list(map(lambda s: weapons_to_name[s], weaknesses)) + valid_weapons = [item for item in progitempool + if item.name in weapons + and item.player == self.player] + placed_weapon = self.random.choice(valid_weapons) + weapon_name = next(name for name, idx in lookup_location_to_id.items() + if idx == 0x880101 + self.options.starting_robot_master.value) + weapon_location = self.get_location(weapon_name) + weapon_location.place_locked_item(placed_weapon) + progitempool.remove(placed_weapon) + fill_locations.remove(weapon_location) + + def generate_output(self, output_directory: str) -> None: + try: + patch = MM2ProcedurePatch(player=self.player, player_name=self.player_name) + patch_rom(self, patch) + + self.rom_name = patch.name + + 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 + + def fill_slot_data(self) -> Dict[str, Any]: + return { + "death_link": self.options.death_link.value, + "weapon_damage": self.weapon_damage, + "wily_5_weapons": self.wily_5_weapons, + } + + def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Dict[str, Any]: + local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()} + local_wily = {int(key): value for key, value in slot_data["wily_5_weapons"].items()} + return {"weapon_damage": local_weapon, "wily_5_weapons": local_wily} + + def modify_multidata(self, multidata: Dict[str, Any]) -> None: + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + 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() + multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name] diff --git a/worlds/mm2/client.py b/worlds/mm2/client.py new file mode 100644 index 000000000000..aaa0813c763a --- /dev/null +++ b/worlds/mm2/client.py @@ -0,0 +1,562 @@ +import logging +import time +from enum import IntEnum +from base64 import b64encode +from typing import TYPE_CHECKING, Dict, Tuple, List, Optional, Any +from NetUtils import ClientStatus, color, NetworkItem +from worlds._bizhawk.client import BizHawkClient + +if TYPE_CHECKING: + from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor + +nes_logger = logging.getLogger("NES") +logger = logging.getLogger("Client") + +MM2_ROBOT_MASTERS_UNLOCKED = 0x8A +MM2_ROBOT_MASTERS_DEFEATED = 0x8B +MM2_ITEMS_ACQUIRED = 0x8C +MM2_LAST_WILY = 0x8D +MM2_RECEIVED_ITEMS = 0x8E +MM2_DEATHLINK = 0x8F +MM2_ENERGYLINK = 0x90 +MM2_RBM_STROBE = 0x91 +MM2_WEAPONS_UNLOCKED = 0x9A +MM2_ITEMS_UNLOCKED = 0x9B +MM2_WEAPON_ENERGY = 0x9C +MM2_E_TANKS = 0xA7 +MM2_LIVES = 0xA8 +MM2_DIFFICULTY = 0xCB +MM2_HEALTH = 0x6C0 +MM2_COMPLETED_STAGES = 0x770 +MM2_CONSUMABLES = 0x780 + +MM2_SFX_QUEUE = 0x580 +MM2_SFX_STROBE = 0x66 + +MM2_CONSUMABLE_TABLE: Dict[int, Tuple[int, int]] = { + # Item: (byte offset, bit mask) + 0x880201: (0, 8), + 0x880202: (16, 1), + 0x880203: (16, 2), + 0x880204: (16, 4), + 0x880205: (16, 8), + 0x880206: (16, 16), + 0x880207: (16, 32), + 0x880208: (16, 64), + 0x880209: (16, 128), + 0x88020A: (20, 1), + 0x88020B: (20, 4), + 0x88020C: (20, 64), + 0x88020D: (21, 1), + 0x88020E: (21, 2), + 0x88020F: (21, 4), + 0x880210: (24, 1), + 0x880211: (24, 2), + 0x880212: (24, 4), + 0x880213: (28, 1), + 0x880214: (28, 2), + 0x880215: (28, 4), + 0x880216: (33, 4), + 0x880217: (33, 8), + 0x880218: (37, 8), + 0x880219: (37, 16), + 0x88021A: (38, 1), + 0x88021B: (38, 2), + 0x880227: (38, 4), + 0x880228: (38, 32), + 0x880229: (38, 128), + 0x88022A: (39, 4), + 0x88022B: (39, 2), + 0x88022C: (39, 1), + 0x88022D: (38, 64), + 0x88022E: (38, 16), + 0x88022F: (38, 8), + 0x88021C: (39, 32), + 0x88021D: (39, 64), + 0x88021E: (39, 128), + 0x88021F: (41, 16), + 0x880220: (42, 2), + 0x880221: (42, 4), + 0x880222: (42, 8), + 0x880223: (46, 1), + 0x880224: (46, 2), + 0x880225: (46, 4), + 0x880226: (46, 8), +} + + +class MM2EnergyLinkType(IntEnum): + Life = 0 + AtomicFire = 1 + AirShooter = 2 + LeafShield = 3 + BubbleLead = 4 + QuickBoomerang = 5 + TimeStopper = 6 + MetalBlade = 7 + CrashBomber = 8 + Item1 = 9 + Item2 = 10 + Item3 = 11 + OneUP = 12 + + +request_to_name: Dict[str, str] = { + "HP": "health", + "AF": "Atomic Fire energy", + "AS": "Air Shooter energy", + "LS": "Leaf Shield energy", + "BL": "Bubble Lead energy", + "QB": "Quick Boomerang energy", + "TS": "Time Stopper energy", + "MB": "Metal Blade energy", + "CB": "Crash Bomber energy", + "I1": "Item 1 energy", + "I2": "Item 2 energy", + "I3": "Item 3 energy", + "1U": "lives" +} + +HP_EXCHANGE_RATE = 500000000 +WEAPON_EXCHANGE_RATE = 250000000 +ONEUP_EXCHANGE_RATE = 14000000000 + + +def cmd_pool(self: "BizHawkClientCommandProcessor") -> None: + """Check the current pool of EnergyLink, and requestable refills from it.""" + if self.ctx.game != "Mega Man 2": + logger.warning("This command can only be used when playing Mega Man 2.") + return + if not self.ctx.server or not self.ctx.slot: + logger.warning("You must be connected to a server to use this command.") + return + energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0) + health_points = energylink // HP_EXCHANGE_RATE + weapon_points = energylink // WEAPON_EXCHANGE_RATE + lives = energylink // ONEUP_EXCHANGE_RATE + logger.info(f"Healing available: {health_points}\n" + f"Weapon refill available: {weapon_points}\n" + f"Lives available: {lives}") + + +def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None: + from worlds._bizhawk.context import BizHawkClientContext + """Request a refill from EnergyLink.""" + if self.ctx.game != "Mega Man 2": + logger.warning("This command can only be used when playing Mega Man 2.") + return + if not self.ctx.server or not self.ctx.slot: + logger.warning("You must be connected to a server to use this command.") + return + valid_targets: Dict[str, MM2EnergyLinkType] = { + "HP": MM2EnergyLinkType.Life, + "AF": MM2EnergyLinkType.AtomicFire, + "AS": MM2EnergyLinkType.AirShooter, + "LS": MM2EnergyLinkType.LeafShield, + "BL": MM2EnergyLinkType.BubbleLead, + "QB": MM2EnergyLinkType.QuickBoomerang, + "TS": MM2EnergyLinkType.TimeStopper, + "MB": MM2EnergyLinkType.MetalBlade, + "CB": MM2EnergyLinkType.CrashBomber, + "I1": MM2EnergyLinkType.Item1, + "I2": MM2EnergyLinkType.Item2, + "I3": MM2EnergyLinkType.Item3, + "1U": MM2EnergyLinkType.OneUP + } + if target.upper() not in valid_targets: + logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}") + return + ctx = self.ctx + assert isinstance(ctx, BizHawkClientContext) + client = ctx.client_handler + assert isinstance(client, MegaMan2Client) + client.refill_queue.append((valid_targets[target.upper()], int(amount))) + logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.") + + +def cmd_autoheal(self) -> None: + """Enable auto heal from EnergyLink.""" + if self.ctx.game != "Mega Man 2": + logger.warning("This command can only be used when playing Mega Man 2.") + return + if not self.ctx.server or not self.ctx.slot: + logger.warning("You must be connected to a server to use this command.") + return + else: + assert isinstance(self.ctx.client_handler, MegaMan2Client) + if self.ctx.client_handler.auto_heal: + self.ctx.client_handler.auto_heal = False + logger.info(f"Auto healing disabled.") + else: + self.ctx.client_handler.auto_heal = True + logger.info(f"Auto healing enabled.") + + +def get_sfx_writes(sfx: int) -> Tuple[Tuple[int, bytes, str], ...]: + return (MM2_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"), (MM2_SFX_STROBE, 0x01.to_bytes(1, "little"), "RAM") + + +class MegaMan2Client(BizHawkClient): + game = "Mega Man 2" + system = "NES" + patch_suffix = ".apmm2" + item_queue: List[NetworkItem] = [] + pending_death_link: bool = False + # default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once + sending_death_link: bool = True + death_link: bool = False + energy_link: bool = False + rom: Optional[bytes] = None + weapon_energy: int = 0 + health_energy: int = 0 + auto_heal: bool = False + refill_queue: List[Tuple[MM2EnergyLinkType, int]] = [] + last_wily: Optional[int] = None # default to wily 1 + + async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: + from worlds._bizhawk import RequestFailedError, read + from . import MM2World + + try: + game_name, version = (await read(ctx.bizhawk_ctx, [(0x3FFB0, 21, "PRG ROM"), + (0x3FFC8, 3, "PRG ROM")])) + if game_name[:3] != b"MM2" or version != bytes(MM2World.world_version): + if game_name[:3] == b"MM2": + # I think this is an easier check than the other? + older_version = "0.2.1" if version == b"\xFF\xFF\xFF" else f"{version[0]}.{version[1]}.{version[2]}" + logger.warning(f"This Mega Man 2 patch was generated for an different version of the apworld. " + f"Please use that version to connect instead.\n" + f"Patch version: ({older_version})\n" + f"Client version: ({'.'.join([str(i) for i in MM2World.world_version])})") + if "pool" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("pool") + if "request" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("request") + if "autoheal" in ctx.command_processor.commands: + ctx.command_processor.commands.pop("autoheal") + return False + except UnicodeDecodeError: + return False + except RequestFailedError: + return False # Should verify on the next pass + + ctx.game = self.game + self.rom = game_name + ctx.items_handling = 0b111 + ctx.want_slot_data = False + deathlink = (await read(ctx.bizhawk_ctx, [(0x3FFC5, 1, "PRG ROM")]))[0][0] + if deathlink & 0x01: + self.death_link = True + if deathlink & 0x02: + self.energy_link = True + + if self.energy_link: + if "pool" not in ctx.command_processor.commands: + ctx.command_processor.commands["pool"] = cmd_pool + if "request" not in ctx.command_processor.commands: + ctx.command_processor.commands["request"] = cmd_request + if "autoheal" not in ctx.command_processor.commands: + ctx.command_processor.commands["autoheal"] = cmd_autoheal + + return True + + async def set_auth(self, ctx: "BizHawkClientContext") -> None: + if self.rom: + ctx.auth = b64encode(self.rom).decode() + + def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: Dict[str, Any]) -> None: + if cmd == "Bounced": + if "tags" in args: + assert ctx.slot is not None + if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name: + self.on_deathlink(ctx) + elif cmd == "Retrieved": + if f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]: + self.last_wily = args["keys"][f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"] + elif cmd == "Connected": + if self.energy_link: + ctx.set_notify(f"EnergyLink{ctx.team}") + if ctx.ui: + ctx.ui.enable_energy_link() + + async def send_deathlink(self, ctx: "BizHawkClientContext") -> None: + self.sending_death_link = True + ctx.last_death_link = time.time() + await ctx.send_death("Mega Man was defeated.") + + def on_deathlink(self, ctx: "BizHawkClientContext") -> None: + ctx.last_death_link = time.time() + self.pending_death_link = True + + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: + from worlds._bizhawk import read, write + + if ctx.server is None: + return + + if ctx.slot is None: + return + + # get our relevant bytes + robot_masters_unlocked, robot_masters_defeated, items_acquired, \ + weapons_unlocked, items_unlocked, items_received, \ + completed_stages, consumable_checks, \ + e_tanks, lives, weapon_energy, health, difficulty, death_link_status, \ + energy_link_packet, last_wily = await read(ctx.bizhawk_ctx, [ + (MM2_ROBOT_MASTERS_UNLOCKED, 1, "RAM"), + (MM2_ROBOT_MASTERS_DEFEATED, 1, "RAM"), + (MM2_ITEMS_ACQUIRED, 1, "RAM"), + (MM2_WEAPONS_UNLOCKED, 1, "RAM"), + (MM2_ITEMS_UNLOCKED, 1, "RAM"), + (MM2_RECEIVED_ITEMS, 1, "RAM"), + (MM2_COMPLETED_STAGES, 0xE, "RAM"), + (MM2_CONSUMABLES, 52, "RAM"), + (MM2_E_TANKS, 1, "RAM"), + (MM2_LIVES, 1, "RAM"), + (MM2_WEAPON_ENERGY, 11, "RAM"), + (MM2_HEALTH, 1, "RAM"), + (MM2_DIFFICULTY, 1, "RAM"), + (MM2_DEATHLINK, 1, "RAM"), + (MM2_ENERGYLINK, 1, "RAM"), + (MM2_LAST_WILY, 1, "RAM"), + ]) + + if difficulty[0] not in (0, 1): + return # Game is not initialized + + if not ctx.finished_game and completed_stages[0xD] != 0: + # this sets on credits fade, no real better way to do this + await ctx.send_msgs([{ + "cmd": "StatusUpdate", + "status": ClientStatus.CLIENT_GOAL + }]) + writes = [] + + # deathlink + if self.death_link: + await ctx.update_death_link(self.death_link) + if self.pending_death_link: + writes.append((MM2_DEATHLINK, bytes([0x01]), "RAM")) + self.pending_death_link = False + self.sending_death_link = True + if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): + if health[0] == 0x00 and not self.sending_death_link: + await self.send_deathlink(ctx) + elif health[0] != 0x00 and not death_link_status[0]: + self.sending_death_link = False + + if self.last_wily != last_wily[0]: + if self.last_wily is None: + # revalidate last wily from data storage + await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [ + {"operation": "default", "value": 8} + ]}]) + await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]}]) + elif last_wily[0] == 0: + writes.append((MM2_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM")) + else: + # correct our setting + self.last_wily = last_wily[0] + await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [ + {"operation": "replace", "value": self.last_wily} + ]}]) + + # handle receiving items + recv_amount = items_received[0] + if recv_amount < len(ctx.items_received): + item = ctx.items_received[recv_amount] + logging.info('Received %s from %s (%s) (%d/%d in list)' % ( + color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), + ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received))) + + if item.item & 0x130 == 0: + # Robot Master Weapon + new_weapons = weapons_unlocked[0] | (1 << ((item.item & 0xF) - 1)) + writes.append((MM2_WEAPONS_UNLOCKED, new_weapons.to_bytes(1, 'little'), "RAM")) + writes.extend(get_sfx_writes(0x21)) + elif item.item & 0x30 == 0: + # Robot Master Stage Access + new_stages = robot_masters_unlocked[0] & ~(1 << ((item.item & 0xF) - 1)) + writes.append((MM2_ROBOT_MASTERS_UNLOCKED, new_stages.to_bytes(1, 'little'), "RAM")) + writes.extend(get_sfx_writes(0x3a)) + writes.append((MM2_RBM_STROBE, b"\x01", "RAM")) + elif item.item & 0x20 == 0: + # Items + new_items = items_unlocked[0] | (1 << ((item.item & 0xF) - 1)) + writes.append((MM2_ITEMS_UNLOCKED, new_items.to_bytes(1, 'little'), "RAM")) + writes.extend(get_sfx_writes(0x21)) + else: + # append to the queue, so we handle it later + self.item_queue.append(item) + recv_amount += 1 + writes.append((MM2_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM")) + + if energy_link_packet[0]: + pickup = energy_link_packet[0] + if pickup in (0x76, 0x77): + # Health pickups + if pickup == 0x77: + value = 2 + else: + value = 10 + exchange_rate = HP_EXCHANGE_RATE + elif pickup in (0x78, 0x79): + # Weapon Energy + if pickup == 0x79: + value = 2 + else: + value = 10 + exchange_rate = WEAPON_EXCHANGE_RATE + elif pickup == 0x7B: + # 1-Up + value = 1 + exchange_rate = ONEUP_EXCHANGE_RATE + else: + # if we managed to pickup something else, we should just fall through + value = 0 + exchange_rate = 0 + contribution = (value * exchange_rate) >> 1 + if contribution: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations": + [{"operation": "add", "value": contribution}, + {"operation": "max", "value": 0}]}]) + logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.") + writes.append((MM2_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM")) + + if self.weapon_energy: + # Weapon Energy + # We parse the whole thing to spread it as thin as possible + current_energy = self.weapon_energy + weapon_energy = bytearray(weapon_energy) + for i, weapon in zip(range(len(weapon_energy)), weapon_energy): + if weapon < 0x1C: + missing = 0x1C - weapon + if missing > self.weapon_energy: + missing = self.weapon_energy + self.weapon_energy -= missing + weapon_energy[i] = weapon + missing + if not self.weapon_energy: + writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM")) + break + else: + if current_energy != self.weapon_energy: + writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM")) + + if self.health_energy or self.auto_heal: + # Health Energy + # We save this if the player has not taken any damage + current_health = health[0] + if 0 < current_health < 0x1C: + health_diff = 0x1C - current_health + if self.health_energy: + if health_diff > self.health_energy: + health_diff = self.health_energy + self.health_energy -= health_diff + else: + pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0) + if health_diff * HP_EXCHANGE_RATE > pool: + health_diff = int(pool // HP_EXCHANGE_RATE) + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations": + [{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE}, + {"operation": "max", "value": 0}]}]) + current_health += health_diff + writes.append((MM2_HEALTH, current_health.to_bytes(1, 'little'), "RAM")) + + if self.refill_queue: + refill_type, refill_amount = self.refill_queue.pop() + if refill_type == MM2EnergyLinkType.Life: + exchange_rate = HP_EXCHANGE_RATE + elif refill_type == MM2EnergyLinkType.OneUP: + exchange_rate = ONEUP_EXCHANGE_RATE + else: + exchange_rate = WEAPON_EXCHANGE_RATE + pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0) + request = exchange_rate * refill_amount + if request > pool: + logger.warning( + f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}") + else: + await ctx.send_msgs([{ + "cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations": + [{"operation": "add", "value": -request}, + {"operation": "max", "value": 0}]}]) + if refill_type == MM2EnergyLinkType.Life: + refill_ptr = MM2_HEALTH + elif refill_type == MM2EnergyLinkType.OneUP: + refill_ptr = MM2_LIVES + else: + refill_ptr = MM2_WEAPON_ENERGY - 1 + refill_type + current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0] + new_value = min(0x1C if refill_type != MM2EnergyLinkType.OneUP else 99, current_value + refill_amount) + writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM")) + + if len(self.item_queue): + item = self.item_queue.pop(0) + idx = item.item & 0xF + if idx == 0: + # 1-Up + current_lives = lives[0] + if current_lives > 99: + self.item_queue.append(item) + else: + current_lives += 1 + writes.append((MM2_LIVES, current_lives.to_bytes(1, 'little'), "RAM")) + writes.extend(get_sfx_writes(0x42)) + elif idx == 1: + self.weapon_energy += 0xE + writes.extend(get_sfx_writes(0x28)) + elif idx == 2: + self.health_energy += 0xE + writes.extend(get_sfx_writes(0x28)) + elif idx == 3: + # E-Tank + # visuals only allow 4, but we're gonna go up to 9 anyway? May change + current_tanks = e_tanks[0] + if current_tanks < 9: + current_tanks += 1 + writes.append((MM2_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM")) + writes.extend(get_sfx_writes(0x42)) + else: + self.item_queue.append(item) + + await write(ctx.bizhawk_ctx, writes) + + new_checks = [] + # check for locations + for i in range(8): + flag = 1 << i + if robot_masters_defeated[0] & flag: + wep_id = 0x880101 + i + if wep_id not in ctx.checked_locations: + new_checks.append(wep_id) + + for i in range(3): + flag = 1 << i + if items_acquired[0] & flag: + itm_id = 0x880111 + i + if itm_id not in ctx.checked_locations: + new_checks.append(itm_id) + + for i in range(0xD): + rbm_id = 0x880001 + i + if completed_stages[i] != 0: + if rbm_id not in ctx.checked_locations: + new_checks.append(rbm_id) + + for consumable in MM2_CONSUMABLE_TABLE: + if consumable not in ctx.checked_locations: + is_checked = consumable_checks[MM2_CONSUMABLE_TABLE[consumable][0]] \ + & MM2_CONSUMABLE_TABLE[consumable][1] + if is_checked: + new_checks.append(consumable) + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names.lookup_in_game(new_check_id) + nes_logger.info( + f'New Check: {location} ({len(ctx.locations_checked)}/' + f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}]) diff --git a/worlds/mm2/color.py b/worlds/mm2/color.py new file mode 100644 index 000000000000..77b39caf3d4f --- /dev/null +++ b/worlds/mm2/color.py @@ -0,0 +1,276 @@ +from typing import Dict, Tuple, List, TYPE_CHECKING, Union +from . import names +from zlib import crc32 +import struct +import logging + +if TYPE_CHECKING: + from . import MM2World + from .rom import MM2ProcedurePatch + +HTML_TO_NES: Dict[str, int] = { + "SNOW": 0x20, + "LINEN": 0x36, + "SEASHELL": 0x36, + "AZURE": 0x3C, + "LAVENDER": 0x33, + "WHITE": 0x30, + "BLACK": 0x0F, + "GREY": 0x00, + "GRAY": 0x00, + "ROYALBLUE": 0x12, + "BLUE": 0x11, + "SKYBLUE": 0x21, + "LIGHTBLUE": 0x31, + "TURQUOISE": 0x2B, + "CYAN": 0x2C, + "AQUAMARINE": 0x3B, + "DARKGREEN": 0x0A, + "GREEN": 0x1A, + "YELLOW": 0x28, + "GOLD": 0x28, + "WHEAT": 0x37, + "TAN": 0x37, + "CHOCOLATE": 0x07, + "BROWN": 0x07, + "SALMON": 0x26, + "ORANGE": 0x27, + "CORAL": 0x36, + "TOMATO": 0x16, + "RED": 0x16, + "PINK": 0x25, + "MAROON": 0x06, + "MAGENTA": 0x24, + "FUSCHIA": 0x24, + "VIOLET": 0x24, + "PLUM": 0x33, + "PURPLE": 0x14, + "THISTLE": 0x34, + "DARKBLUE": 0x01, + "SILVER": 0x10, + "NAVY": 0x02, + "TEAL": 0x1C, + "OLIVE": 0x18, + "LIME": 0x2A, + "AQUA": 0x2C, + # can add more as needed +} + +MM2_COLORS: Dict[str, Tuple[int, int]] = { + names.atomic_fire: (0x28, 0x15), + names.air_shooter: (0x20, 0x11), + names.leaf_shield: (0x20, 0x19), + names.bubble_lead: (0x20, 0x00), + names.time_stopper: (0x34, 0x25), + names.quick_boomerang: (0x34, 0x14), + names.metal_blade: (0x37, 0x18), + names.crash_bomber: (0x20, 0x26), + names.item_1: (0x20, 0x16), + names.item_2: (0x20, 0x16), + names.item_3: (0x20, 0x16), + names.heat_man_stage: (0x28, 0x15), + names.air_man_stage: (0x28, 0x11), + names.wood_man_stage: (0x36, 0x17), + names.bubble_man_stage: (0x30, 0x19), + names.quick_man_stage: (0x28, 0x15), + names.flash_man_stage: (0x30, 0x12), + names.metal_man_stage: (0x28, 0x15), + names.crash_man_stage: (0x30, 0x16) +} + +MM2_KNOWN_COLORS: Dict[str, Tuple[int, int]] = { + **MM2_COLORS, + # Street Fighter, technically + "Hadouken": (0x3C, 0x11), + "Shoryuken": (0x38, 0x16), + # X Series + "Z-Saber": (0x20, 0x16), + # X1 + "Homing Torpedo": (0x3D, 0x37), + "Chameleon Sting": (0x3B, 0x1A), + "Rolling Shield": (0x3A, 0x25), + "Fire Wave": (0x37, 0x26), + "Storm Tornado": (0x34, 0x14), + "Electric Spark": (0x3D, 0x28), + "Boomerang Cutter": (0x3B, 0x2D), + "Shotgun Ice": (0x28, 0x2C), + # X2 + "Crystal Hunter": (0x33, 0x21), + "Bubble Splash": (0x35, 0x28), + "Spin Wheel": (0x34, 0x1B), + "Silk Shot": (0x3B, 0x27), + "Sonic Slicer": (0x27, 0x01), + "Strike Chain": (0x30, 0x23), + "Magnet Mine": (0x28, 0x2D), + "Speed Burner": (0x31, 0x16), + # X3 + "Acid Burst": (0x28, 0x2A), + "Tornado Fang": (0x28, 0x2C), + "Triad Thunder": (0x2B, 0x23), + "Spinning Blade": (0x20, 0x16), + "Ray Splasher": (0x28, 0x17), + "Gravity Well": (0x38, 0x14), + "Parasitic Bomb": (0x31, 0x28), + "Frost Shield": (0x23, 0x2C), +} + +palette_pointers: Dict[str, List[int]] = { + "Mega Buster": [0x3D314], + "Atomic Fire": [0x3D318], + "Air Shooter": [0x3D31C], + "Leaf Shield": [0x3D320], + "Bubble Lead": [0x3D324], + "Quick Boomerang": [0x3D328], + "Time Stopper": [0x3D32C], + "Metal Blade": [0x3D330], + "Crash Bomber": [0x3D334], + "Item 1": [0x3D338], + "Item 2": [0x3D33C], + "Item 3": [0x3D340], + "Heat Man": [0x34B6, 0x344F7], + "Air Man": [0x74B6, 0x344FF], + "Wood Man": [0xB4EC, 0x34507], + "Bubble Man": [0xF4B6, 0x3450F], + "Quick Man": [0x134C8, 0x34517], + "Flash Man": [0x174B6, 0x3451F], + "Metal Man": [0x1B4A4, 0x34527], + "Crash Man": [0x1F4EC, 0x3452F], +} + + +def add_color_to_mm2(name: str, color: Tuple[int, int]) -> None: + """ + Add a color combo for Mega Man 2 to recognize as the color to display for a given item. + For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02 + """ + MM2_KNOWN_COLORS[name] = validate_colors(*color) + + +def extrapolate_color(color: int) -> Tuple[int, int]: + if color > 0x1F: + color_1 = color + color_2 = color_1 - 0x10 + else: + color_2 = color + color_1 = color_2 + 0x10 + return color_1, color_2 + + +def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> Tuple[int, int]: + # Black should be reserved for outlines, a gray should suffice + if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]: + color_1 = 0x10 + if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]: + color_2 = 0x10 + + # one final check, make sure we don't have two matching + if not allow_match and color_1 == color_2: + color_1 = 0x30 # color 1 to white works with about any paired color + + return color_1, color_2 + + +def get_colors_for_item(name: str) -> Tuple[int, int]: + if name in MM2_KNOWN_COLORS: + return MM2_KNOWN_COLORS[name] + + check_colors = {color: color in name.upper().replace(" ", "") for color in HTML_TO_NES} + colors = [color for color in check_colors if check_colors[color]] + if colors: + # we have at least one color pattern matched + if len(colors) > 1: + # we have at least 2 + color_1 = HTML_TO_NES[colors[0]] + color_2 = HTML_TO_NES[colors[1]] + else: + color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]]) + else: + # generate hash + crc_hash = crc32(name.encode("utf-8")) + hash_color = struct.pack("I", crc_hash) + color_1 = hash_color[0] % 0x3F + color_2 = hash_color[1] % 0x3F + + if color_1 < color_2: + temp = color_1 + color_1 = color_2 + color_2 = temp + + color_1, color_2 = validate_colors(color_1, color_2) + + return color_1, color_2 + + +def parse_color(colors: List[str]) -> Tuple[int, int]: + color_a = colors[0] + if color_a.startswith("$"): + color_1 = int(color_a[1:], 16) + else: + # assume it's in our list of colors + color_1 = HTML_TO_NES[color_a.upper()] + + if len(colors) == 1: + color_1, color_2 = extrapolate_color(color_1) + else: + color_b = colors[1] + if color_b.startswith("$"): + color_2 = int(color_b[1:], 16) + else: + color_2 = HTML_TO_NES[color_b.upper()] + return color_1, color_2 + + +def write_palette_shuffle(world: "MM2World", rom: "MM2ProcedurePatch") -> None: + palette_shuffle: Union[int, str] = world.options.palette_shuffle.value + palettes_to_write: Dict[str, Tuple[int, int]] = {} + if isinstance(palette_shuffle, str): + color_sets = palette_shuffle.split(";") + if len(color_sets) == 1: + palette_shuffle = world.options.palette_shuffle.option_none + # singularity is more correct, but this is faster + else: + palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()] + for color_set in color_sets: + if "-" in color_set: + character, color = color_set.split("-") + if character.title() not in palette_pointers: + logging.warning(f"Player {world.multiworld.get_player_name(world.player)} " + f"attempted to set color for unrecognized option {character}") + colors = color.split("|") + real_colors = validate_colors(*parse_color(colors), allow_match=True) + palettes_to_write[character.title()] = real_colors + else: + # If color is provided with no character, assume singularity + colors = color_set.split("|") + real_colors = validate_colors(*parse_color(colors), allow_match=True) + for character in palette_pointers: + palettes_to_write[character] = real_colors + # Now we handle the real values + if palette_shuffle == 1: + shuffled_colors = list(MM2_COLORS.values()) + shuffled_colors.append((0x2C, 0x11)) # Mega Buster + world.random.shuffle(shuffled_colors) + for character in palette_pointers: + if character not in palettes_to_write: + palettes_to_write[character] = shuffled_colors.pop() + elif palette_shuffle > 1: + if palette_shuffle == 2: + for character in palette_pointers: + if character not in palettes_to_write: + real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F)) + palettes_to_write[character] = real_colors + else: + # singularity + real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F)) + for character in palette_pointers: + if character not in palettes_to_write: + palettes_to_write[character] = real_colors + + for character in palettes_to_write: + for pointer in palette_pointers[character]: + rom.write_bytes(pointer, bytes(palettes_to_write[character])) + + if character == "Atomic Fire": + # special case, we need to update Atomic Fire's flashing routine + rom.write_byte(0x3DE4A, palettes_to_write[character][1]) + rom.write_byte(0x3DE4C, palettes_to_write[character][1]) diff --git a/worlds/mm2/data/mm2_basepatch.bsdiff4 b/worlds/mm2/data/mm2_basepatch.bsdiff4 new file mode 100644 index 000000000000..8f3c17c3c7af Binary files /dev/null and b/worlds/mm2/data/mm2_basepatch.bsdiff4 differ diff --git a/worlds/mm2/docs/en_Mega Man 2.md b/worlds/mm2/docs/en_Mega Man 2.md new file mode 100644 index 000000000000..2c9504f5d00f --- /dev/null +++ b/worlds/mm2/docs/en_Mega Man 2.md @@ -0,0 +1,114 @@ +# Mega Man 2 + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Weapons received from Robot Masters, access to each individual stage, and Items from Dr. Light are randomized +into the multiworld. Access to the Wily Stages is locked behind receiving Item 1, 2, and 3. The game is completed when +viewing the ending sequence after defeating the Alien. + +## What Mega Man 2 items can appear in other players' worlds? +- Robot Master weapons +- Robot Master Access Codes (stage access) +- Items 1/2/3 +- 1-Ups +- E-Tanks +- Health Energy (L) +- Weapon Energy (L) + +## What is considered a location check in Mega Man 2? +- The defeat of a Robot Master or Wily Boss +- Receiving a weapon or item from Dr. Light +- Optionally, 1-Ups and E-Tanks present within stages +- Optionally, Weapon and Health Energy pickups present within stages + +## When the player receives an item, what happens? +A sound effect will play based on the type of item received, and the effects of the item will be immediately applied, +such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving +Health Energy while at full health), the leftover amount is withheld until it can be applied. + +## What is EnergyLink? +EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man + 2, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink. +Half of the energy that would be gained is lost upon transfer to the EnergyLink. + +Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates. +You can find out how much of each type you can pull using the `/pool` command in the client. Additionally, you can have it +automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client. +Finally, you can use the `/request` command to request a certain type of energy from the storage. + +## Plando Palettes +The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing +so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of +the following: +- Mega Buster +- Atomic Fire +- Air Shooter +- Leaf Shield +- Bubble Lead +- Quick Boomerang +- Time Stopper +- Metal Blade +- Crash Bomber +- Item 1 +- Item 2 +- Item 3 +- Heat Man +- Air Man +- Wood Man +- Bubble Man +- Quick Man +- Flash Man +- Metal Man +- Crash Man + +Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be +found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/color.py#L11). Alternatively, colors can +be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02). + +You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color +given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to +all weapons/bosses that did not have a prior color specified. + +The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any +plando placements. + +## Plando Weaknesses +Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior +weaknesses generated by strict/random weakness options. Formatting for this is as follows: +```yaml +plando_weakness: + Air Man: + Atomic Fire: 0 + Bubble Lead: 4 +``` +This would cause Air Man to take 4 damage from Bubble Lead, and 0 from Atomic Fire. + +Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game +becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the +Robot Master. + + +## Unique Local Commands +- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled. +- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to +restore Mega Man's health. +- `/request ` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from +the EnergyLink. Types are as follows: + - `HP` Health + - `AF` Atomic Fire + - `AS` Air Shooter + - `LS` Leaf Shield + - `BL` Bubble Lead + - `QB` Quick Boomerang + - `TS` Time Stopper + - `MB` Metal Blade + - `CB` Crash Bomber + - `I1` Item 1 + - `I2` Item 2 + - `I3` Item 3 + - `1U` Lives \ No newline at end of file diff --git a/worlds/mm2/docs/setup_en.md b/worlds/mm2/docs/setup_en.md new file mode 100644 index 000000000000..3b8f833b9967 --- /dev/null +++ b/worlds/mm2/docs/setup_en.md @@ -0,0 +1,53 @@ +# Mega Man 2 Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- An English Mega Man 2 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later + +### Configuring Bizhawk + +Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings: + +- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from +`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.) +- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're +tabbed out of EmuHawk. +- Open a `.nes` file in EmuHawk and go to `Config > Controllersâ€Ļ` to configure your inputs. If you can't click +`Controllersâ€Ļ`, load any `.nes` ROM first. +- Consider clearing keybinds in `Config > Hotkeysâ€Ļ` if you don't intend to use them. Select the keybind and press Esc to +clear it. + +## Generating and Patching a Game + +1. Create your options file (YAML). You can make one on the +[Mega Man 2 options page](../../../games/Mega%20Man%202/player-options). +2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). +This will generate an output file for you. Your patch file will have the `.apmm2` file extension. +3. Open `ArchipelagoLauncher.exe` +4. Select "Open Patch" on the left side and select your patch file. +5. If this is your first time patching, you will be prompted to locate your vanilla ROM. If you are using the Legacy +Collection, provide `Proteus.exe` in place of your rom. +6. A patched `.nes` file will be created in the same place as the patch file. +7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your +BizHawk install. + +## Connecting to a Server + +By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just +in case you have to close and reopen a window mid-game for some reason. + +1. Mega Man 2 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game, +you can re-open it from the launcher. +2. Ensure EmuHawk is running the patched ROM. +3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. +4. In the Lua Console window, go to `Script > Open Scriptâ€Ļ`. +5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`. +6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it +connected and recognized Mega Man 2. +7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the +top text field of the client and click Connect. + +You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is +perfectly safe to make progress offline; everything will re-sync when you reconnect. diff --git a/worlds/mm2/items.py b/worlds/mm2/items.py new file mode 100644 index 000000000000..e644b171dded --- /dev/null +++ b/worlds/mm2/items.py @@ -0,0 +1,72 @@ +from BaseClasses import Item +from typing import NamedTuple, Dict +from . import names + + +class ItemData(NamedTuple): + code: int + progression: bool + useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade + skip_balancing: bool = False + + +class MM2Item(Item): + game = "Mega Man 2" + + +robot_master_weapon_table = { + names.atomic_fire: ItemData(0x880001, True), + names.air_shooter: ItemData(0x880002, True), + names.leaf_shield: ItemData(0x880003, True), + names.bubble_lead: ItemData(0x880004, True), + names.quick_boomerang: ItemData(0x880005, True), + names.time_stopper: ItemData(0x880006, True, True), + names.metal_blade: ItemData(0x880007, True, True), + names.crash_bomber: ItemData(0x880008, True), +} + +stage_access_table = { + names.heat_man_stage: ItemData(0x880101, True), + names.air_man_stage: ItemData(0x880102, True), + names.wood_man_stage: ItemData(0x880103, True), + names.bubble_man_stage: ItemData(0x880104, True), + names.quick_man_stage: ItemData(0x880105, True), + names.flash_man_stage: ItemData(0x880106, True), + names.metal_man_stage: ItemData(0x880107, True), + names.crash_man_stage: ItemData(0x880108, True), +} + +item_item_table = { + names.item_1: ItemData(0x880011, True, True, True), + names.item_2: ItemData(0x880012, True, True, True), + names.item_3: ItemData(0x880013, True, True, True) +} + +filler_item_table = { + names.one_up: ItemData(0x880020, False), + names.weapon_energy: ItemData(0x880021, False), + names.health_energy: ItemData(0x880022, False), + names.e_tank: ItemData(0x880023, False, True), +} + +filler_item_weights = { + names.one_up: 1, + names.weapon_energy: 4, + names.health_energy: 1, + names.e_tank: 2, +} + +item_table = { + **robot_master_weapon_table, + **stage_access_table, + **item_item_table, + **filler_item_table, +} + +item_names = { + "Weapons": {name for name in robot_master_weapon_table.keys()}, + "Stages": {name for name in stage_access_table.keys()}, + "Items": {name for name in item_item_table.keys()} +} + +lookup_item_to_id: Dict[str, int] = {item_name: data.code for item_name, data in item_table.items()} diff --git a/worlds/mm2/locations.py b/worlds/mm2/locations.py new file mode 100644 index 000000000000..4807d25d6992 --- /dev/null +++ b/worlds/mm2/locations.py @@ -0,0 +1,239 @@ +from BaseClasses import Location, Region +from typing import Dict, Tuple, Optional +from . import names + + +class MM2Location(Location): + game = "Mega Man 2" + + +class MM2Region(Region): + game = "Mega Man 2" + + +heat_man_locations: Dict[str, Optional[int]] = { + names.heat_man: 0x880001, + names.atomic_fire_get: 0x880101, + names.item_1_get: 0x880111, +} + +air_man_locations: Dict[str, Optional[int]] = { + names.air_man: 0x880002, + names.air_shooter_get: 0x880102, + names.item_2_get: 0x880112 +} + +wood_man_locations: Dict[str, Optional[int]] = { + names.wood_man: 0x880003, + names.leaf_shield_get: 0x880103 +} + +bubble_man_locations: Dict[str, Optional[int]] = { + names.bubble_man: 0x880004, + names.bubble_lead_get: 0x880104 +} + +quick_man_locations: Dict[str, Optional[int]] = { + names.quick_man: 0x880005, + names.quick_boomerang_get: 0x880105, +} + +flash_man_locations: Dict[str, Optional[int]] = { + names.flash_man: 0x880006, + names.time_stopper_get: 0x880106, + names.item_3_get: 0x880113, +} + +metal_man_locations: Dict[str, Optional[int]] = { + names.metal_man: 0x880007, + names.metal_blade_get: 0x880107 +} + +crash_man_locations: Dict[str, Optional[int]] = { + names.crash_man: 0x880008, + names.crash_bomber_get: 0x880108 +} + +wily_1_locations: Dict[str, Optional[int]] = { + names.wily_1: 0x880009, + names.wily_stage_1: None +} + +wily_2_locations: Dict[str, Optional[int]] = { + names.wily_2: 0x88000A, + names.wily_stage_2: None +} + +wily_3_locations: Dict[str, Optional[int]] = { + names.wily_3: 0x88000B, + names.wily_stage_3: None +} + +wily_4_locations: Dict[str, Optional[int]] = { + names.wily_4: 0x88000C, + names.wily_stage_4: None +} + +wily_5_locations: Dict[str, Optional[int]] = { + names.wily_5: 0x88000D, + names.wily_stage_5: None +} + +wily_6_locations: Dict[str, Optional[int]] = { + names.dr_wily: None +} + +etank_1ups: Dict[str, Dict[str, Optional[int]]] = { + "Heat Man Stage": { + names.heat_man_c1: 0x880201, + }, + "Quick Man Stage": { + names.quick_man_c1: 0x880202, + names.quick_man_c2: 0x880203, + names.quick_man_c3: 0x880204, + names.quick_man_c7: 0x880208, + }, + "Flash Man Stage": { + names.flash_man_c2: 0x88020B, + names.flash_man_c6: 0x88020F, + }, + "Metal Man Stage": { + names.metal_man_c1: 0x880210, + names.metal_man_c2: 0x880211, + names.metal_man_c3: 0x880212, + }, + "Crash Man Stage": { + names.crash_man_c2: 0x880214, + names.crash_man_c3: 0x880215, + }, + "Wily Stage 1": { + names.wily_1_c1: 0x880216, + }, + "Wily Stage 2": { + names.wily_2_c3: 0x88021A, + names.wily_2_c4: 0x88021B, + names.wily_2_c5: 0x88021C, + names.wily_2_c6: 0x88021D, + }, + "Wily Stage 3": { + names.wily_3_c2: 0x880220, + }, + "Wily Stage 4": { + names.wily_4_c3: 0x880225, + names.wily_4_c4: 0x880226, + } +} + +energy_pickups: Dict[str, Dict[str, Optional[int]]] = { + "Quick Man Stage": { + names.quick_man_c4: 0x880205, + names.quick_man_c5: 0x880206, + names.quick_man_c6: 0x880207, + names.quick_man_c8: 0x880209, + }, + "Flash Man Stage": { + names.flash_man_c1: 0x88020A, + names.flash_man_c3: 0x88020C, + names.flash_man_c4: 0x88020D, + names.flash_man_c5: 0x88020E, + }, + "Crash Man Stage": { + names.crash_man_c1: 0x880213, + }, + "Wily Stage 1": { + names.wily_1_c2: 0x880217, + }, + "Wily Stage 2": { + names.wily_2_c1: 0x880218, + names.wily_2_c2: 0x880219, + names.wily_2_c7: 0x88021E, + names.wily_2_c8: 0x880227, + names.wily_2_c9: 0x880228, + names.wily_2_c10: 0x880229, + names.wily_2_c11: 0x88022A, + names.wily_2_c12: 0x88022B, + names.wily_2_c13: 0x88022C, + names.wily_2_c14: 0x88022D, + names.wily_2_c15: 0x88022E, + names.wily_2_c16: 0x88022F, + }, + "Wily Stage 3": { + names.wily_3_c1: 0x88021F, + names.wily_3_c3: 0x880221, + names.wily_3_c4: 0x880222, + }, + "Wily Stage 4": { + names.wily_4_c1: 0x880223, + names.wily_4_c2: 0x880224, + } +} + +mm2_regions: Dict[str, Tuple[Tuple[str, ...], Dict[str, Optional[int]], Optional[str]]] = { + "Heat Man Stage": ((names.heat_man_stage,), heat_man_locations, None), + "Air Man Stage": ((names.air_man_stage,), air_man_locations, None), + "Wood Man Stage": ((names.wood_man_stage,), wood_man_locations, None), + "Bubble Man Stage": ((names.bubble_man_stage,), bubble_man_locations, None), + "Quick Man Stage": ((names.quick_man_stage,), quick_man_locations, None), + "Flash Man Stage": ((names.flash_man_stage,), flash_man_locations, None), + "Metal Man Stage": ((names.metal_man_stage,), metal_man_locations, None), + "Crash Man Stage": ((names.crash_man_stage,), crash_man_locations, None), + "Wily Stage 1": ((names.item_1, names.item_2, names.item_3), wily_1_locations, None), + "Wily Stage 2": ((names.wily_stage_1,), wily_2_locations, "Wily Stage 1"), + "Wily Stage 3": ((names.wily_stage_2,), wily_3_locations, "Wily Stage 2"), + "Wily Stage 4": ((names.wily_stage_3,), wily_4_locations, "Wily Stage 3"), + "Wily Stage 5": ((names.wily_stage_4,), wily_5_locations, "Wily Stage 4"), + "Wily Stage 6": ((names.wily_stage_5,), wily_6_locations, "Wily Stage 5") +} + +location_table: Dict[str, Optional[int]] = { + **heat_man_locations, + **air_man_locations, + **wood_man_locations, + **bubble_man_locations, + **quick_man_locations, + **flash_man_locations, + **metal_man_locations, + **crash_man_locations, + **wily_1_locations, + **wily_2_locations, + **wily_3_locations, + **wily_4_locations, + **wily_5_locations, +} + +for table in etank_1ups: + location_table.update(etank_1ups[table]) + +for table in energy_pickups: + location_table.update(energy_pickups[table]) + +location_groups = { + "Get Equipped": { + names.atomic_fire_get, + names.air_shooter_get, + names.leaf_shield_get, + names.bubble_lead_get, + names.quick_boomerang_get, + names.time_stopper_get, + names.metal_blade_get, + names.crash_bomber_get, + names.item_1_get, + names.item_2_get, + names.item_3_get + }, + "Heat Man Stage": {*heat_man_locations.keys(), *etank_1ups["Heat Man Stage"].keys()}, + "Air Man Stage": {*air_man_locations.keys()}, + "Wood Man Stage": {*wood_man_locations.keys()}, + "Bubble Man Stage": {*bubble_man_locations.keys()}, + "Quick Man Stage": {*quick_man_locations.keys(), *etank_1ups["Quick Man Stage"].keys(), + *energy_pickups["Quick Man Stage"].keys()}, + "Flash Man Stage": {*flash_man_locations.keys(), *etank_1ups["Flash Man Stage"].keys(), + *energy_pickups["Flash Man Stage"].keys()}, + "Metal Man Stage": {*metal_man_locations.keys(), *etank_1ups["Metal Man Stage"].keys()}, + "Crash Man Stage": {*crash_man_locations.keys(), *etank_1ups["Crash Man Stage"].keys(), + *energy_pickups["Crash Man Stage"].keys()}, + "Wily 2 Weapon Energy": {names.wily_2_c8, names.wily_2_c9, names.wily_2_c10, names.wily_2_c11, names.wily_2_c12, + names.wily_2_c13, names.wily_2_c14, names.wily_2_c15, names.wily_2_c16} +} + +lookup_location_to_id: Dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None} diff --git a/worlds/mm2/names.py b/worlds/mm2/names.py new file mode 100644 index 000000000000..fbbea85f0317 --- /dev/null +++ b/worlds/mm2/names.py @@ -0,0 +1,114 @@ +# Robot Master Weapons +crash_bomber = "Crash Bomber" +metal_blade = "Metal Blade" +quick_boomerang = "Quick Boomerang" +bubble_lead = "Bubble Lead" +atomic_fire = "Atomic Fire" +leaf_shield = "Leaf Shield" +time_stopper = "Time Stopper" +air_shooter = "Air Shooter" + +# Stage Entry +crash_man_stage = "Crash Man Access Codes" +metal_man_stage = "Metal Man Access Codes" +quick_man_stage = "Quick Man Access Codes" +bubble_man_stage = "Bubble Man Access Codes" +heat_man_stage = "Heat Man Access Codes" +wood_man_stage = "Wood Man Access Codes" +flash_man_stage = "Flash Man Access Codes" +air_man_stage = "Air Man Access Codes" + +# The Items +item_1 = "Item 1 - Propeller" +item_2 = "Item 2 - Rocket" +item_3 = "Item 3 - Bouncy" + +# Misc. Items +one_up = "1-Up" +weapon_energy = "Weapon Energy (L)" +health_energy = "Health Energy (L)" +e_tank = "E-Tank" + +# Locations +crash_man = "Crash Man - Defeated" +metal_man = "Metal Man - Defeated" +quick_man = "Quick Man - Defeated" +bubble_man = "Bubble Man - Defeated" +heat_man = "Heat Man - Defeated" +wood_man = "Wood Man - Defeated" +flash_man = "Flash Man - Defeated" +air_man = "Air Man - Defeated" +crash_bomber_get = "Crash Bomber - Received" +metal_blade_get = "Metal Blade - Received" +quick_boomerang_get = "Quick Boomerang - Received" +bubble_lead_get = "Bubble Lead - Received" +atomic_fire_get = "Atomic Fire - Received" +leaf_shield_get = "Leaf Shield - Received" +time_stopper_get = "Time Stopper - Received" +air_shooter_get = "Air Shooter - Received" +item_1_get = "Item 1 - Received" +item_2_get = "Item 2 - Received" +item_3_get = "Item 3 - Received" +wily_1 = "Mecha Dragon - Defeated" +wily_2 = "Picopico-kun - Defeated" +wily_3 = "Guts Tank - Defeated" +wily_4 = "Boobeam Trap - Defeated" +wily_5 = "Wily Machine 2 - Defeated" +dr_wily = "Dr. Wily (Alien) - Defeated" + +# Wily Stage Event Items +wily_stage_1 = "Wily Stage 1 - Completed" +wily_stage_2 = "Wily Stage 2 - Completed" +wily_stage_3 = "Wily Stage 3 - Completed" +wily_stage_4 = "Wily Stage 4 - Completed" +wily_stage_5 = "Wily Stage 5 - Completed" + +# Consumable Locations +heat_man_c1 = "Heat Man Stage - 1-Up" # 3, requires Yoku jumps or Item 2 +flash_man_c1 = "Flash Man Stage - Health Energy 1" # 0 +flash_man_c2 = "Flash Man Stage - 1-Up" # 2, requires any Item +flash_man_c3 = "Flash Man Stage - Health Energy 2" # 6, requires Crash Bomber +flash_man_c4 = "Flash Man Stage - Weapon Energy 1" # 8, requires Crash Bomber +flash_man_c5 = "Flash Man Stage - Health Energy 3" # 9 +flash_man_c6 = "Flash Man Stage - E-Tank" # 10 +quick_man_c1 = "Quick Man Stage - 1-Up 1" # 0, needs any Item +quick_man_c2 = "Quick Man Stage - E-Tank" # 1, requires allow lasers or Time Stopper +quick_man_c3 = "Quick Man Stage - 1-Up 2" # 2, requires allow lasers or Time Stopper +quick_man_c4 = "Quick Man Stage - Weapon Energy 1" # 3, requires allow lasers or Time Stopper +quick_man_c5 = "Quick Man Stage - Weapon Energy 2" # 4, requires allow lasers or Time Stopper +quick_man_c6 = "Quick Man Stage - Health Energy" # 5, requires allow lasers or Time Stopper +quick_man_c7 = "Quick Man Stage - 1-Up 3" # 6, requires allow lasers or Time Stopper +quick_man_c8 = "Quick Man Stage - Weapon Energy 3" # 7, requires allow lasers or Time Stopper +metal_man_c1 = "Metal Man Stage - E-Tank 1" # 0 +metal_man_c2 = "Metal Man Stage - 1-Up" # 1, needs Item 1/2 +metal_man_c3 = "Metal Man Stage - E-Tank 2" # 2, needs Item 1/2 (without putting dying in logic at least) +crash_man_c1 = "Crash Man Stage - Health Energy" # 0 +crash_man_c2 = "Crash Man Stage - E-Tank" # 1 +crash_man_c3 = "Crash Man Stage - 1-Up" # 2, any Item +wily_1_c1 = "Wily Stage 1 - 1-Up" # 10 +wily_1_c2 = "Wily Stage 1 - Weapon Energy 1" # 11 +wily_2_c1 = "Wily Stage 2 - Weapon Energy 1" # 11 +wily_2_c2 = "Wily Stage 2 - Weapon Energy 2" # 12 +wily_2_c3 = "Wily Stage 2 - E-Tank 1" # 16 +wily_2_c4 = "Wily Stage 2 - 1-Up 1" # 17 +# 18 - 27 are all small weapon energies, might force these local junk? +wily_2_c8 = "Wily Stage 2 - Weapon Energy 3" # 18 +wily_2_c9 = "Wily Stage 2 - Weapon Energy 4" # 19 +wily_2_c10 = "Wily Stage 2 - Weapon Energy 5" # 20 +wily_2_c11 = "Wily Stage 2 - Weapon Energy 6" # 21 +wily_2_c12 = "Wily Stage 2 - Weapon Energy 7" # 22 +wily_2_c13 = "Wily Stage 2 - Weapon Energy 8" # 23 +wily_2_c14 = "Wily Stage 2 - Weapon Energy 9" # 24 +wily_2_c15 = "Wily Stage 2 - Weapon Energy 10" # 25 +wily_2_c16 = "Wily Stage 2 - Weapon Energy 11" # 26 +wily_2_c5 = "Wily Stage 2 - 1-Up 2" # 29, requires Crash Bomber +wily_2_c6 = "Wily Stage 2 - E-Tank 2" # 30, requires Crash Bomber +wily_2_c7 = "Wily Stage 2 - Health Energy" # 31, item 2 (already required to reach wily 2) +wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # 12, requires Crash Bomber +wily_3_c2 = "Wily Stage 3 - E-Tank" # 17, requires Crash Bomber +wily_3_c3 = "Wily Stage 3 - Weapon Energy 2" # 18 +wily_3_c4 = "Wily Stage 3 - Weapon Energy 3" # 19 +wily_4_c1 = "Wily Stage 4 - Weapon Energy 1" # 16 +wily_4_c2 = "Wily Stage 4 - Weapon Energy 2" # 17 +wily_4_c3 = "Wily Stage 4 - 1-Up 1" # 18 +wily_4_c4 = "Wily Stage 4 - E-Tank 1" # 19 diff --git a/worlds/mm2/options.py b/worlds/mm2/options.py new file mode 100644 index 000000000000..2d90395cacda --- /dev/null +++ b/worlds/mm2/options.py @@ -0,0 +1,229 @@ +from dataclasses import dataclass + +from Options import Choice, Toggle, DeathLink, DefaultOnToggle, TextChoice, Range, OptionDict, PerGameCommonOptions +from schema import Schema, And, Use, Optional + +bosses = { + "Heat Man": 0, + "Air Man": 1, + "Wood Man": 2, + "Bubble Man": 3, + "Quick Man": 4, + "Flash Man": 5, + "Metal Man": 6, + "Crash Man": 7, + "Mecha Dragon": 8, + "Picopico-kun": 9, + "Guts Tank": 10, + "Boobeam Trap": 11, + "Wily Machine 2": 12, + "Alien": 13 +} + +weapons_to_id = { + "Mega Buster": 0, + "Atomic Fire": 1, + "Air Shooter": 2, + "Leaf Shield": 3, + "Bubble Lead": 4, + "Quick Boomerang": 5, + "Metal Blade": 7, + "Crash Bomber": 6, + "Time Stopper": 8, +} + + +class EnergyLink(Toggle): + """ + Enables EnergyLink support. + When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can + be requested from the EnergyLink pool. + Some of the energy sent to the pool will be lost on transfer. + """ + display_name = "EnergyLink" + + +class StartingRobotMaster(Choice): + """ + The initial stage unlocked at the start. + """ + display_name = "Starting Robot Master" + option_heat_man = 0 + option_air_man = 1 + option_wood_man = 2 + option_bubble_man = 3 + option_quick_man = 4 + option_flash_man = 5 + option_metal_man = 6 + option_crash_man = 7 + default = "random" + + +class YokuJumps(Toggle): + """ + When enabled, the player is expected to be able to perform the yoku block sequence in Heat Man's + stage without Item 2. + """ + display_name = "Yoku Block Jumps" + + +class EnableLasers(Toggle): + """ + When enabled, the player is expected to complete (and acquire items within) the laser sections of Quick Man's + stage without the Time Stopper. + """ + display_name = "Enable Lasers" + + +class Consumables(Choice): + """ + When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks. + E-Tanks and 1-Ups add 20 checks to the pool. + Weapon/Health Energy add 27 checks to the pool. + """ + display_name = "Consumables" + option_none = 0 + option_1up_etank = 1 + option_weapon_health = 2 + option_all = 3 + default = 1 + alias_true = 3 + alias_false = 0 + + @classmethod + def get_option_name(cls, value: int) -> str: + if value == 1: + return "1-Ups/E-Tanks" + if value == 2: + return "Weapon/Health Energy" + return super().get_option_name(value) + + +class Quickswap(DefaultOnToggle): + """ + When enabled, the player can quickswap through all received weapons by pressing Select. + """ + display_name = "Quickswap" + + +class PaletteShuffle(TextChoice): + """ + Change the color of Mega Man and the Robot Masters. + None: The palettes are unchanged. + Shuffled: Palette colors are shuffled amongst the robot masters. + Randomized: Random (usually good) palettes are generated for each robot master. + Singularity: one palette is generated and used for all robot masters. + Supports custom palettes using HTML named colors in the + following format: Mega Buster-Lavender|Violet;randomized + The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for + that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with + a semicolon. + """ + display_name = "Palette Shuffle" + option_none = 0 + option_shuffled = 1 + option_randomized = 2 + option_singularity = 3 + + +class EnemyWeaknesses(Toggle): + """ + Randomizes the damage dealt to enemies by weapons. Friender will always take damage from the buster. + """ + display_name = "Random Enemy Weaknesses" + + +class StrictWeaknesses(Toggle): + """ + Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons. + Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Alien). + """ + display_name = "Strict Boss Weaknesses" + + +class RandomWeaknesses(Choice): + """ + None: Bosses will have their regular weaknesses. + Shuffled: Weapon damage will be shuffled amongst the weapons, so Metal Blade may do Bubble Lead damage. + Time Stopper will deplete half of a random Robot Master's HP. + Randomized: Weapon damage will be fully randomized. + """ + display_name = "Random Boss Weaknesses" + option_none = 0 + option_shuffled = 1 + option_randomized = 2 + alias_false = 0 + alias_true = 2 + + +class Wily5Requirement(Range): + """Change the number of Robot Masters that are required to be defeated for + the teleporter to the Wily Machine to appear.""" + display_name = "Wily 5 Requirement" + default = 8 + range_start = 1 + range_end = 8 + + +class WeaknessPlando(OptionDict): + """ + Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses. + plando_weakness: + Robot Master: + Weapon: Damage + """ + display_name = "Plando Weaknesses" + schema = Schema({ + Optional(And(str, Use(str.title), lambda s: s in bosses)): { + And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 14)) + } + }) + default = {} + + +class ReduceFlashing(Choice): + """ + Reduce flashing seen in gameplay, such as the stage select and when defeating a Wily boss. + Virtual Console: increases length of most flashes, changes some flashes from white to a dark gray. + Minor: VC changes + decreasing the speed of Bubble/Metal Man stage animations. + Full: VC changes + further decreasing the brightness of most flashes and + disables stage animations for Metal/Bubble Man stages. + """ + display_name = "Reduce Flashing" + option_none = 0 + option_virtual_console = 1 + option_minor = 2 + option_full = 3 + default = 1 + + +class RandomMusic(Choice): + """ + Vanilla: music is unchanged + Shuffled: stage and certain menu music is shuffled. + Randomized: stage and certain menu music is randomly selected + None: no music will play + """ + display_name = "Random Music" + option_vanilla = 0 + option_shuffled = 1 + option_randomized = 2 + option_none = 3 + +@dataclass +class MM2Options(PerGameCommonOptions): + death_link: DeathLink + energy_link: EnergyLink + starting_robot_master: StartingRobotMaster + consumables: Consumables + yoku_jumps: YokuJumps + enable_lasers: EnableLasers + enemy_weakness: EnemyWeaknesses + strict_weakness: StrictWeaknesses + random_weakness: RandomWeaknesses + wily_5_requirement: Wily5Requirement + plando_weakness: WeaknessPlando + palette_shuffle: PaletteShuffle + quickswap: Quickswap + reduce_flashing: ReduceFlashing + random_music: RandomMusic diff --git a/worlds/mm2/rom.py b/worlds/mm2/rom.py new file mode 100644 index 000000000000..e37c5bc2a148 --- /dev/null +++ b/worlds/mm2/rom.py @@ -0,0 +1,415 @@ +import pkgutil +from typing import Optional, TYPE_CHECKING, Iterable, Dict, Sequence +import hashlib +import Utils +import os + +import settings +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes +from . import names +from .rules import minimum_weakness_requirement +from .text import MM2TextEntry +from .color import get_colors_for_item, write_palette_shuffle +from .options import Consumables, ReduceFlashing, RandomMusic + +if TYPE_CHECKING: + from . import MM2World + +MM2LCHASH = "37f2c36ce7592f1e16b3434b3985c497" +PROTEUSHASH = "9ff045a3ca30018b6e874c749abb3ec4" +MM2NESHASH = "0527a0ee512f69e08b8db6dc97964632" +MM2VCHASH = "0c78dfe8e90fb8f3eed022ff01126ad3" + +enemy_weakness_ptrs: Dict[int, int] = { + 0: 0x3E9A8, + 1: 0x3EA24, + 2: 0x3EA9C, + 3: 0x3EB14, + 4: 0x3EB8C, + 5: 0x3EC04, + 6: 0x3EC7C, + 7: 0x3ECF4, +} + +enemy_addresses: Dict[str, int] = { + "Shrink": 0x00, + "M-445": 0x04, + "Claw": 0x08, + "Tanishi": 0x0A, + "Kerog": 0x0C, + "Petit Kerog": 0x0D, + "Anko": 0x0F, + "Batton": 0x16, + "Robitto": 0x17, + "Friender": 0x1C, + "Monking": 0x1D, + "Kukku": 0x1F, + "Telly": 0x22, + "Changkey Maker": 0x23, + "Changkey": 0x24, + "Pierrobot": 0x29, + "Fly Boy": 0x2C, + # "Crash Wall": 0x2D + # "Friender Wall": 0x2E + "Blocky": 0x31, + "Neo Metall": 0x34, + "Matasaburo": 0x36, + "Pipi": 0x38, + "Pipi Egg": 0x3A, + "Copipi": 0x3C, + "Kaminari Goro": 0x3E, + "Petit Goblin": 0x45, + "Springer": 0x46, + "Mole (Up)": 0x48, + "Mole (Down)": 0x49, + "Shotman (Left)": 0x4B, + "Shotman (Right)": 0x4C, + "Sniper Armor": 0x4E, + "Sniper Joe": 0x4F, + "Scworm": 0x50, + "Scworm Worm": 0x51, + "Picopico-kun": 0x6A, + "Boobeam Trap": 0x6D, + "Big Fish": 0x71 +} + +# addresses printed when assembling basepatch +consumables_ptr: int = 0x3F2FE +quickswap_ptr: int = 0x3F363 +wily_5_ptr: int = 0x3F3A1 +energylink_ptr: int = 0x3F46B +get_equipped_sound_ptr: int = 0x3F384 + + +class RomData: + def __init__(self, file: bytes, name: 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: Sequence[int]) -> None: + self.file[offset:offset + len(values)] = values + + def write_to_file(self, file: str) -> None: + with open(file, 'wb') as outfile: + outfile.write(self.file) + + +class MM2ProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [MM2LCHASH, MM2NESHASH, MM2VCHASH] + game = "Mega Man 2" + patch_file_ending = ".apmm2" + result_file_ending = ".nes" + name: bytearray + procedure = [ + ("apply_bsdiff4", ["mm2_basepatch.bsdiff4"]), + ("apply_tokens", ["token_patch.bin"]), + ] + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + def write_byte(self, offset: int, value: int) -> None: + self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little")) + + def write_bytes(self, offset: int, value: Iterable[int]) -> None: + self.write_token(APTokenTypes.WRITE, offset, bytes(value)) + + +def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None: + patch.write_file("mm2_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mm2_basepatch.bsdiff4")) + # text writing + patch.write_bytes(0x37E2A, MM2TextEntry("FOR ", 0xCB).resolve()) + patch.write_bytes(0x37EAA, MM2TextEntry("GET EQUIPPED ", 0x0B).resolve()) + patch.write_bytes(0x37EBA, MM2TextEntry("WITH ", 0x2B).resolve()) + + base_address = 0x3F650 + color_address = 0x37F6C + for i, location in zip(range(11), [ + names.atomic_fire_get, + names.air_shooter_get, + names.leaf_shield_get, + names.bubble_lead_get, + names.quick_boomerang_get, + names.time_stopper_get, + names.metal_blade_get, + names.crash_bomber_get, + names.item_1_get, + names.item_2_get, + names.item_3_get + ]): + item = world.multiworld.get_location(location, world.player).item + if item: + if len(item.name) <= 14: + # we want to just place it in the center + first_str = "" + second_str = item.name + third_str = "" + elif len(item.name) <= 28: + # spread across second and third + first_str = "" + second_str = item.name[:14] + third_str = item.name[14:] + else: + # all three + first_str = item.name[:14] + second_str = item.name[14:28] + third_str = item.name[28:] + if len(third_str) > 16: + third_str = third_str[:16] + player_str = world.multiworld.get_player_name(item.player) + if len(player_str) > 14: + player_str = player_str[:14] + patch.write_bytes(base_address + (64 * i), MM2TextEntry(first_str, 0x4B).resolve()) + patch.write_bytes(base_address + (64 * i) + 16, MM2TextEntry(second_str, 0x6B).resolve()) + patch.write_bytes(base_address + (64 * i) + 32, MM2TextEntry(third_str, 0x8B).resolve()) + patch.write_bytes(base_address + (64 * i) + 48, MM2TextEntry(player_str, 0xEB).resolve()) + + colors = get_colors_for_item(item.name) + if i > 7: + patch.write_bytes(color_address + 27 + ((i - 8) * 2), colors) + else: + patch.write_bytes(color_address + (i * 2), colors) + + write_palette_shuffle(world, patch) + + enemy_weaknesses: Dict[str, Dict[int, int]] = {} + + if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness: + # we need to write boss weaknesses + output = bytearray() + for weapon in world.weapon_damage: + if weapon == 8: + continue # Time Stopper is a special case + weapon_damage = [world.weapon_damage[weapon][i] + if world.weapon_damage[weapon][i] >= 0 + else 256 + world.weapon_damage[weapon][i] + for i in range(14)] + output.extend(weapon_damage) + patch.write_bytes(0x2E952, bytes(output)) + time_stopper_damage = world.weapon_damage[8] + time_offset = 0x2C03B + damage_table = { + 4: 0xF, + 3: 0x17, + 2: 0x1E, + 1: 0x25 + } + for boss, damage in enumerate(time_stopper_damage): + if damage > 4: + damage = 4 # 4 is a guaranteed kill, no need to exceed + if damage <= 0: + patch.write_byte(time_offset + 14 + boss, 0) + else: + patch.write_byte(time_offset + 14 + boss, 1) + patch.write_byte(time_offset + boss, damage_table[damage]) + if world.options.random_weakness: + wily_5_weaknesses = [i for i in range(8) if world.weapon_damage[i][12] > minimum_weakness_requirement[i]] + world.random.shuffle(wily_5_weaknesses) + if len(wily_5_weaknesses) >= 3: + weak1 = wily_5_weaknesses.pop() + weak2 = wily_5_weaknesses.pop() + weak3 = wily_5_weaknesses.pop() + elif len(wily_5_weaknesses) == 2: + weak1 = weak2 = wily_5_weaknesses.pop() + weak3 = wily_5_weaknesses.pop() + else: + weak1 = weak2 = weak3 = 0 + patch.write_byte(0x2DA2E, weak1) + patch.write_byte(0x2DA32, weak2) + patch.write_byte(0x2DA3A, weak3) + enemy_weaknesses["Picopico-kun"] = {weapon: world.weapon_damage[weapon][9] for weapon in range(8)} + enemy_weaknesses["Boobeam Trap"] = {weapon: world.weapon_damage[weapon][11] for weapon in range(8)} + + if world.options.enemy_weakness: + for enemy in enemy_addresses: + if enemy in ("Picopico-kun", "Boobeam Trap"): + continue + enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs} + if enemy == "Friender": + # Friender has to be killed, need buster damage to not break logic + enemy_weaknesses[enemy][0] = max(enemy_weaknesses[enemy][0], 1) + + for enemy, damage_table in enemy_weaknesses.items(): + for weapon in enemy_weakness_ptrs: + if damage_table[weapon] < 0: + damage_table[weapon] = 256 + damage_table[weapon] + patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage_table[weapon]) + + if world.options.quickswap: + patch.write_byte(quickswap_ptr + 1, 0x01) + + if world.options.consumables != Consumables.option_all: + value_a = 0x7C + value_b = 0x76 + if world.options.consumables == Consumables.option_1up_etank: + value_b = 0x7A + else: + value_a = 0x7A + patch.write_byte(consumables_ptr - 3, value_a) + patch.write_byte(consumables_ptr + 1, value_b) + + patch.write_byte(wily_5_ptr + 1, world.options.wily_5_requirement.value) + + if world.options.energy_link: + patch.write_byte(energylink_ptr + 1, 1) + + if world.options.reduce_flashing: + if world.options.reduce_flashing.value == ReduceFlashing.option_virtual_console: + color = 0x2D # Dark Gray + speed = -1 + elif world.options.reduce_flashing.value == ReduceFlashing.option_minor: + color = 0x2D + speed = 0x08 + else: + color = 0x0F + speed = 0x00 + patch.write_byte(0x2D1B0, color) # Change white to a dark gray, Mecha Dragon + patch.write_byte(0x2D397, 0x0F) # Longer flash time, Mecha Dragon kill + patch.write_byte(0x2D3A0, color) # Change white to a dark gray, Picopico-kun/Boobeam Trap + patch.write_byte(0x2D65F, color) # Change white to a dark gray, Guts Tank + patch.write_byte(0x2DA94, color) # Change white to a dark gray, Wily Machine + patch.write_byte(0x2DC97, color) # Change white to a dark gray, Alien + patch.write_byte(0x2DD68, 0x10) # Longer flash time, Alien kill + patch.write_bytes(0x2DF14, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA]) # Reduce final Alien flash to 1 big flash + patch.write_byte(0x34132, 0x08) # Longer flash time, Stage Select + + if world.options.reduce_flashing.value == ReduceFlashing.option_full: + # reduce color of stage flashing + patch.write_bytes(0x344C9, [0x2D, 0x10, 0x00, 0x2D, + 0x0F, 0x10, 0x2D, 0x00, + 0x0F, 0x10, 0x2D, 0x00, + 0x0F, 0x10, 0x2D, 0x00, + 0x2D, 0x10, 0x2D, 0x00, + 0x0F, 0x10, 0x2D, 0x00, + 0x0F, 0x10, 0x2D, 0x00, + 0x0F, 0x10, 0x2D, 0x00]) + # remove wily castle flash + patch.write_byte(0x3596D, 0x0F) + + if speed != -1: + patch.write_byte(0xFE01, speed) # Bubble Man Stage + patch.write_byte(0x1BE01, speed) # Metal Man Stage + + if world.options.random_music: + if world.options.random_music == RandomMusic.option_none: + pool = [0xFF] * 20 + # A couple of additional mutes we want here + patch.write_byte(0x37819, 0xFF) # Credits + patch.write_byte(0x378A4, 0xFF) # Credits #2 + patch.write_byte(0x37149, 0xFF) # Game Over Jingle + patch.write_byte(0x341BA, 0xFF) # Robot Master Jingle + patch.write_byte(0x2E0B4, 0xFF) # Robot Master Defeated + patch.write_byte(0x35B78, 0xFF) # Wily Castle + patch.write_byte(0x2DFA5, 0xFF) # Wily Defeated + + elif world.options.random_music == RandomMusic.option_shuffled: + pool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 9, 0x10, 0xC, 0xB, 0x17, 0x13, 0xE, 0xD] + world.random.shuffle(pool) + else: + pool = world.random.choices([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xB, 0xC, 0xD, 0xE, 0x10, 0x13, 0x17], k=20) + patch.write_bytes(0x381E0, pool[:13]) + patch.write_byte(0x36318, pool[13]) # Game Start + patch.write_byte(0x37181, pool[13]) # Game Over + patch.write_byte(0x340AE, pool[14]) # RBM Select + patch.write_byte(0x39005, pool[15]) # Robot Master Battle + patch.write_byte(get_equipped_sound_ptr + 1, pool[16]) # Get Equipped, we actually hook this already lmao + patch.write_byte(0x3775A, pool[17]) # Epilogue + patch.write_byte(0x36089, pool[18]) # Intro + patch.write_byte(0x361F1, pool[19]) # Title + + + + from Utils import __version__ + patch.name = bytearray(f'MM2{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', + 'utf8')[:21] + patch.name.extend([0] * (21 - len(patch.name))) + patch.write_bytes(0x3FFC0, patch.name) + deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1) + patch.write_byte(0x3FFD5, deathlink_byte) + + patch.write_bytes(0x3FFD8, world.world_version) + + version_map = { + "0": 0x90, + "1": 0x91, + "2": 0x92, + "3": 0x93, + "4": 0x94, + "5": 0x95, + "6": 0x96, + "7": 0x97, + "8": 0x98, + "9": 0x99, + ".": 0xDC + } + patch.write_token(APTokenTypes.RLE, 0x36EE0, (11, 0)) + patch.write_token(APTokenTypes.RLE, 0x36EEE, (25, 0)) + + # BY SILVRIS + patch.write_bytes(0x36EE0, [0xC2, 0xD9, 0xC0, 0xD3, 0xC9, 0xCC, 0xD6, 0xD2, 0xC9, 0xD3]) + # ARCHIPELAGO x.x.x + patch.write_bytes(0x36EF2, [0xC1, 0xD2, 0xC3, 0xC8, 0xC9, 0xD0, 0xC5, 0xCC, 0xC1, 0xC7, 0xCF, 0xC0]) + patch.write_bytes(0x36EFE, list(map(lambda c: version_map[c], __version__))) + + patch.write_file("token_patch.bin", patch.get_token_binary()) + + +header = b"\x4E\x45\x53\x1A\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + +def read_headerless_nes_rom(rom: bytes) -> bytes: + if rom[:4] == b"NES\x1A": + return rom[16:] + else: + return rom + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = read_headerless_nes_rom(bytes(open(file_name, "rb").read())) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() == PROTEUSHASH: + base_rom_bytes = extract_mm2(base_rom_bytes) + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if basemd5.hexdigest() not in {MM2LCHASH, MM2NESHASH, MM2VCHASH}: + print(basemd5.hexdigest()) + raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. " + "Get the correct game and version, then dump it") + headered_rom = bytearray(base_rom_bytes) + headered_rom[0:0] = header + setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom)) + return bytes(headered_rom) + 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["mm2_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name + + +PRG_OFFSET = 0x8ED70 +PRG_SIZE = 0x40000 + + +def extract_mm2(proteus: bytes) -> bytes: + mm2 = bytearray(proteus[PRG_OFFSET:PRG_OFFSET + PRG_SIZE]) + return bytes(mm2) diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py new file mode 100644 index 000000000000..7e2ce1f3c752 --- /dev/null +++ b/worlds/mm2/rules.py @@ -0,0 +1,324 @@ +from math import ceil +from typing import TYPE_CHECKING, Dict, List +from . import names +from .locations import heat_man_locations, air_man_locations, wood_man_locations, bubble_man_locations, \ + quick_man_locations, flash_man_locations, metal_man_locations, crash_man_locations, wily_1_locations, \ + wily_2_locations, wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations +from .options import bosses, weapons_to_id, Consumables, RandomWeaknesses +from worlds.generic.Rules import add_rule + +if TYPE_CHECKING: + from . import MM2World + from BaseClasses import CollectionState + +weapon_damage: Dict[int, List[int]] = { + 0: [2, 2, 1, 1, 2, 2, 1, 1, 1, 7, 1, 0, 1, -1], # Mega Buster + 1: [-1, 6, 0xE, 0, 0xA, 6, 4, 6, 8, 13, 8, 0, 0xE, -1], # Atomic Fire + 2: [2, 0, 4, 0, 2, 0, 0, 0xA, 0, 0, 0, 0, 1, -1], # Air Shooter + 3: [0, 8, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1], # Leaf Shield + 4: [6, 0, 0, -1, 0, 2, 0, 1, 0, 14, 1, 0, 0, 1], # Bubble Lead + 5: [2, 2, 0, 2, 0, 0, 4, 1, 1, 7, 2, 0, 1, -1], # Quick Boomerang + 6: [-1, 0, 2, 2, 4, 3, 0, 0, 1, 0, 1, 0x14, 1, -1], # Crash Bomber + 7: [1, 0, 2, 4, 0, 4, 0xE, 0, 0, 7, 0, 0, 1, -1], # Metal Blade + 8: [0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], # Time Stopper +} + +weapons_to_name: Dict[int, str] = { + 1: names.atomic_fire, + 2: names.air_shooter, + 3: names.leaf_shield, + 4: names.bubble_lead, + 5: names.quick_boomerang, + 6: names.crash_bomber, + 7: names.metal_blade, + 8: names.time_stopper +} + +minimum_weakness_requirement: Dict[int, int] = { + 0: 1, # Mega Buster is free + 1: 14, # 2 shots of Atomic Fire + 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 + 6: 4, # 7 uses of Crash Bomber + 7: 1, # 112 uses of Metal Blade + 8: 4, # 1 use of Time Stopper, but setting to 4 means we shave the entire HP bar +} + +robot_masters: Dict[int, str] = { + 0: "Heat Man Defeated", + 1: "Air Man Defeated", + 2: "Wood Man Defeated", + 3: "Bubble Man Defeated", + 4: "Quick Man Defeated", + 5: "Flash Man Defeated", + 6: "Metal Man Defeated", + 7: "Crash Man Defeated" +} + +weapon_costs = { + 0: 0, + 1: 10, + 2: 2, + 3: 3, + 4: 0.5, + 5: 0.125, + 6: 4, + 7: 0.25, + 8: 7, +} + + +def can_defeat_enough_rbms(state: "CollectionState", player: int, + required: int, boss_requirements: Dict[int, List[int]]): + can_defeat = 0 + for boss, reqs in boss_requirements.items(): + if boss in robot_masters: + if state.has_all(map(lambda x: weapons_to_name[x], reqs), player): + can_defeat += 1 + if can_defeat >= required: + return True + return False + + +def set_rules(world: "MM2World") -> None: + # most rules are set on region, so we only worry about rules required within stage access + # or rules variable on settings + if (hasattr(world.multiworld, "re_gen_passthrough") + and "Mega Man 2" in getattr(world.multiworld, "re_gen_passthrough")): + slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 2"] + world.weapon_damage = slot_data["weapon_damage"] + world.wily_5_weapons = slot_data["wily_5_weapons"] + else: + if world.options.random_weakness == RandomWeaknesses.option_shuffled: + weapon_tables = [table for weapon, table in weapon_damage.items() if weapon not in (0, 8)] + world.random.shuffle(weapon_tables) + for i in range(1, 8): + world.weapon_damage[i] = weapon_tables.pop() + # alien must take minimum required damage from his weakness + alien_weakness = next(weapon for weapon in range(8) if world.weapon_damage[weapon][13] != -1) + world.weapon_damage[alien_weakness][13] = minimum_weakness_requirement[alien_weakness] + world.weapon_damage[8] = [0 for _ in range(14)] + world.weapon_damage[8][world.random.choice(range(8))] = 2 + elif world.options.random_weakness == RandomWeaknesses.option_randomized: + world.weapon_damage = {i: [] for i in range(9)} + for boss in range(13): + for weapon in world.weapon_damage: + world.weapon_damage[weapon].append(min(14, max(-1, int(world.random.normalvariate(3, 3))))) + if not any([world.weapon_damage[weapon][boss] >= max(4, minimum_weakness_requirement[weapon]) + for weapon in range(1, 7)]): + # failsafe, there should be at least one defined non-Buster weakness + weapon = world.random.randint(1, 7) + world.weapon_damage[weapon][boss] = world.random.randint( + max(4, minimum_weakness_requirement[weapon]), 14) # Force weakness + # special case, if boobeam trap has a weakness to Crash, it needs to be max damage + if world.weapon_damage[6][11] > 4: + world.weapon_damage[6][11] = 14 + # handle the alien + boss = 13 + for weapon in world.weapon_damage: + world.weapon_damage[weapon].append(-1) + weapon = world.random.choice(list(world.weapon_damage.keys())) + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + + if world.options.strict_weakness: + for weapon in weapon_damage: + for i in range(13): + if weapon == 0: + world.weapon_damage[weapon][i] = 0 + elif i in (8, 12) and not world.options.random_weakness: + continue + # Mecha Dragon only has damage range of 0-1, so allow the 1 + # Wily Machine needs all three weaknesses present, so allow + elif 4 > world.weapon_damage[weapon][i] > 0: + world.weapon_damage[weapon][i] = 0 + + for p_boss in world.options.plando_weakness: + for p_weapon in world.options.plando_weakness[p_boss]: + if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[p_weapon] \ + and not any(w != p_weapon + and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w] + for w in world.weapon_damage): + # we need to replace this weakness + weakness = world.random.choice([key for key in world.weapon_damage if key != p_weapon]) + world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness] + world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \ + = world.options.plando_weakness[p_boss][p_weapon] + + # handle special cases + for boss in range(14): + for weapon in (1, 2, 3, 6, 8): + if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and + not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon] + for i in range(9) if i != weapon)): + # Weapon does not have enough possible ammo to kill the boss, raise the damage + if boss == 9: + if weapon in (1, 6): + # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + elif boss == 11: + if weapon == 1: + # Atomic Fire cannot be Boobeam Trap's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + else: + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + + if world.weapon_damage[0][world.options.starting_robot_master.value] < 1: + world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value] + + # final special case + # There's a vanilla crash if Time Stopper kills Wily phase 1 + # There's multiple fixes, but ensuring Wily cannot take Time Stopper damage is best + if world.weapon_damage[8][12] > 0: + world.weapon_damage[8][12] = 0 + + # weakness validation, it is better to confirm a completable seed than respect plando + boss_health = {boss: 0x1C if boss != 12 else 0x1C * 2 for boss in [*range(8), 12]} + + weapon_energy = {key: float(0x1C) for key in weapon_costs} + weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage} + for boss in [*range(8), 12]} + flexibility = { + boss: ( + sum(damage_value > 0 for damage_value in + weapon_damages.values()) # Amount of weapons that hit this boss + * sum(weapon_damages.values()) # Overall damage that those weapons do + ) + for boss, weapon_damages in weapon_boss.items() if boss != 12 + } + flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value + used_weapons = {i: set() for i in [*range(8), 12]} + for boss in [*flexibility, 12]: + boss_damage = weapon_boss[boss] + weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in + boss_damage.items() if weapon_energy[weapon] > 0} + if boss_damage[8]: + boss_damage[8] = 1.75 * boss_damage[8] + if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight: + # We get exactly one use of Time Stopper during the rush + # So we want to make sure that use is absolutely needed + weapon_weight[8] = min(weapon_weight[8], 0.001) + while boss_health[boss] > 0: + if boss_damage[0] > 0: + boss_health[boss] = 0 # if we can buster, we should buster + continue + highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) + uses = weapon_energy[wp] // weapon_costs[wp] + if int(uses * boss_damage[wp]) > boss_health[boss]: + used = ceil(boss_health[boss] / boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * used + boss_health[boss] = 0 + used_weapons[boss].add(wp) + elif highest <= 0: + # we are out of weapons that can actually damage the boss + # so find the weapon that has the most uses, and apply that as an additional weakness + # it should be impossible to be out of energy, simply because even if every boss took 1 from + # Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should + # be able to cover + wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight + if weapon != 0 and (weapon != 8 or boss != 12)) + # Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this + world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp] + used = min(int(weapon_energy[wp] // weapon_costs[wp]), + ceil(boss_health[boss] / minimum_weakness_requirement[wp])) + weapon_energy[wp] -= weapon_costs[wp] * used + boss_health[boss] -= int(used * minimum_weakness_requirement[wp]) + weapon_weight.pop(wp) + used_weapons[boss].add(wp) + else: + # drain the weapon and continue + boss_health[boss] -= int(uses * boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * uses + weapon_weight.pop(wp) + used_weapons[boss].add(wp) + + world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons} + + for i, boss_locations in enumerate([ + heat_man_locations, + air_man_locations, + wood_man_locations, + bubble_man_locations, + quick_man_locations, + flash_man_locations, + metal_man_locations, + crash_man_locations, + wily_1_locations, + wily_2_locations, + wily_3_locations, + wily_4_locations, + wily_5_locations, + wily_6_locations + ]): + if world.weapon_damage[0][i] > 0: + continue # this can always be in logic + weapons = [] + for weapon in range(1, 9): + if world.weapon_damage[weapon][i] > 0: + if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]: + continue # Atomic Fire can only be considered logical for bosses it can kill in 2 hits + weapons.append(weapons_to_name[weapon]) + if not weapons: + raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}") + for location in boss_locations: + if i == 12: + add_rule(world.get_location(location), + lambda state, weps=tuple(weapons): state.has_all(weps, world.player)) + # TODO: when has_list gets added, check for a subset of possible weaknesses + else: + add_rule(world.get_location(location), + lambda state, weps=tuple(weapons): state.has_any(weps, world.player)) + + # Always require Crash Bomber for Boobeam Trap + add_rule(world.get_location(names.wily_4), + lambda state: state.has(names.crash_bomber, world.player)) + add_rule(world.get_location(names.wily_stage_4), + lambda state: state.has(names.crash_bomber, world.player)) + + # Need to defeat x amount of robot masters for Wily 5 + add_rule(world.get_location(names.wily_5), + lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value, + world.wily_5_weapons)) + add_rule(world.get_location(names.wily_stage_5), + lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value, + world.wily_5_weapons)) + + if not world.options.yoku_jumps: + add_rule(world.get_entrance("To Heat Man Stage"), + lambda state: state.has(names.item_2, world.player)) + + if not world.options.enable_lasers: + add_rule(world.get_entrance("To Quick Man Stage"), + lambda state: state.has(names.time_stopper, world.player)) + + if world.options.consumables in (Consumables.option_1up_etank, + Consumables.option_all): + add_rule(world.get_location(names.flash_man_c2), + lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player)) + add_rule(world.get_location(names.quick_man_c1), + lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player)) + add_rule(world.get_location(names.metal_man_c2), + lambda state: state.has_any([names.item_1, names.item_2], world.player)) + add_rule(world.get_location(names.metal_man_c3), + lambda state: state.has_any([names.item_1, names.item_2], world.player)) + add_rule(world.get_location(names.crash_man_c3), + lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player)) + add_rule(world.get_location(names.wily_2_c5), + lambda state: state.has(names.crash_bomber, world.player)) + add_rule(world.get_location(names.wily_2_c6), + lambda state: state.has(names.crash_bomber, world.player)) + add_rule(world.get_location(names.wily_3_c2), + lambda state: state.has(names.crash_bomber, world.player)) + if world.options.consumables in (Consumables.option_weapon_health, + Consumables.option_all): + add_rule(world.get_location(names.flash_man_c3), + lambda state: state.has(names.crash_bomber, world.player)) + add_rule(world.get_location(names.flash_man_c4), + lambda state: state.has(names.crash_bomber, world.player)) + add_rule(world.get_location(names.wily_3_c1), + lambda state: state.has(names.crash_bomber, world.player)) diff --git a/worlds/mm2/src/mm2_basepatch.asm b/worlds/mm2/src/mm2_basepatch.asm new file mode 100644 index 000000000000..00c8500f03df --- /dev/null +++ b/worlds/mm2/src/mm2_basepatch.asm @@ -0,0 +1,861 @@ +norom +!headersize = 16 + +!controller_mirror = $23 +!controller_flip = $27 ; only on first frame of input, used by crash man, etc +!current_stage = $2A +!received_stages = $8A +!completed_stages = $8B +!received_item_checks = $8C +!last_wily = $8D +!deathlink = $8F +!energylink_packet = $90 +!rbm_strobe = $91 +!received_weapons = $9A +!received_items = $9B +!current_weapon = $A9 + +!stage_completion = $0F70 +!consumable_checks = $0F80 + +!CONTROLLER_SELECT = #$04 +!CONTROLLER_SELECT_START = #$0C +!CONTROLLER_ALL_BUTTON = #$0F + +!PpuControl_2000 = $2000 +!PpuMask_2001 = $2001 +!PpuAddr_2006 = $2006 +!PpuData_2007 = $2007 + +!LOAD_BANK = $C000 + +macro org(address,bank) + if == $0F + org
-$C000+($4000*)+!headersize ; org sets the position in the output file to write to (in norom, at least) + base
; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere + else + org
-$8000+($4000*)+!headersize + base
+ endif +endmacro + +%org($8400, $08) +incbin "mm2font.dat" + +%org($A900, $09) +incbin "mm2titlefont.dat" + +%org($807E, $0B) +FlashFixes: + CMP #$FF + BEQ FlashFixTarget1 + CMP #$FF + BNE FlashFixTarget2 + +%org($8086, $0B) +FlashFixTarget1: + +%org($808D, $0B) +FlashFixTarget2: + +%org($8015, $0D) +ClearRefreshHook: + ; if we're already doing a fresh load of the stage select + ; we don't need to immediately refresh it + JSR ClearRefresh + NOP + +%org($802B, $0D) +PatchFaceTiles: + LDA !received_stages + +%org($8072, $0D) +PatchFaceSprites: + LDA !received_stages + +%org($80CC, $0D) +CheckItemsForWily: + LDA !received_items + CMP #$07 + +%org($80D2, $0D) +LoadWily: + JSR GoToMostRecentWily + NOP + +%org($80DC, $0D) +CheckAccessCodes: + LDA !received_stages + +%org($8312, $0D) +HookStageSelect: + JSR RefreshRBMTiles + NOP + +%org($A315, $0D) +RemoveWeaponClear: + NOP + NOP + NOP + NOP + +;Adjust Password select flasher +%org($A32A, $0D) + LDX #$68 + +;Block password input +%org($A346, $0D) + EOR #$00 + +;Remove password text +%org($AF3A, $0D) +StartHeight: + db $AC ; set Start to center + +%org($AF49, $0D) +PasswordText: + db $40, $40, $40, $40, $40, $40, $40, $40 + +%org($AF6C, $0D) +ContinueHeight: + db $AB ; split height between 2 remaining options + +%org($AF77, $0D) +StageSelectHeight: + db $EB ; split between 2 remaining options + +%org($AF88, $0D) +GameOverPasswordText: + db $40, $40, $40, $40, $40, $40, $40, $40 + +%org($AFA5, $0D) +GetEquippedPasswordText: + db $40, $40, $40, $40, $40, $40, $40, $40 + +%org($AFAE, $0D) +GetEquippedStageSelect: + db $26, $EA + +%org($B195, $0D) +GameOverPasswordUp: + LDA #$01 ; originally 02, removing last option + +%org($B19F, $0D) +GameOverPassword: + CMP #$02 ; originally 03, remove the last option + +%org($B1ED, $0D) +FixupGameOverArrows: + db $68, $78 + +%org($BB74, $0D) +GetEquippedStage: + JSR StageGetEquipped + NOP #13 + +%org($BBD9, $0D) +GetEquippedDefault: + LDA #$01 + +%org($BC01, $0D) +GetEquippedPasswordRemove: + ORA #$01 ; originally EOR #$01, we always want 1 here + +%org($BCF1, $0D) +GetEquippedItem: + ADC #$07 + JSR ItemGetEquipped + JSR LoadItemsColor + NOP ; !!!! This is a load-bearing NOP. It gets branched to later in the function + LDX $FF + + +%org($BB08, $0D) +WilyProgress: + JSR StoreWilyProgress + NOP + +%org($BF6F, $0D) +GetEquippedStageSelectHeight: + db $B8 + +%org($805B, $0E) +InitalizeStartingRBM: + LDA #$FF ; this does two things + STA !received_stages ; we're overwriting clearing e-tanks and setting RBM available to none + +%org($8066, $0E) +BlockStartupAutoWily: + ; presumably this would be called from password? + LDA #$00 + +%org($80A7, $0E) +StageLoad: + JMP CleanWily5 + NOP + +%org($8178, $0E) +Main1: + JSR MainLoopHook + NOP + +%org($81DE, $0E) +Wily5Teleporter: + LDA $99 + CMP #$01 + BCC SkipSpawn + +%org($81F9, $0E) +SkipSpawn: +; just present to fix the branch, if we try to branch raw it'll get confused + +%org($822D, $0E) +Main2: + ; believe used in the wily 5 refights? + JSR MainLoopHook + NOP + +%org($842F, $0E) +Wily5Hook: + JMP Wily5Requirement + NOP + +%org($C10D, $0F) +Deathlink: + JSR KillMegaMan + +%org($C1BC, $0F) +RemoveETankLoss: + NOP + NOP + +%org($C23C, $0F) +WriteStageComplete: + ORA !completed_stages + STA !completed_stages + +%org($C243, $0F) +WriteReceiveItem: + ORA !received_item_checks + STA !received_item_checks + +%org($C254, $0F) +BlockAutoWily: + ; and this one is on return from stage? + LDA #$00 + +%org($C261, $0F) +WilyStageCompletion: + JSR StoreWilyStageCompletion + NOP + +%org($E5AC, $0F) +NullDeathlink: + STA $8F ; we null his HP later in the process + NOP + +%org($E5D1, $0F) +EnergylinkHook: + JSR Energylink + NOP #2 ; comment this out to enable item giving their usual reward alongside EL + +%org($E5E8, $0F) +ConsumableHook: + JSR CheckConsumable + +%org($F2E3, $0F) + +CheckConsumable: + STA $0140, Y + TXA + PHA + LDA $AD ; the consumable value + CMP #$7C + BPL .Store + print "Consumables (replace 7a): ", hex(realbase()) + CMP #$76 + BMI .Store + LDA #$00 + .Store: + STA $AD + LDA $2A + ASL + ASL + TAX + TYA + .LoopHead: + CMP #$08 + BMI .GetFlag + INX + SBC #$08 + BNE .LoopHead + .GetFlag: + TAY + LDA #$01 + .Loop2Head: + CPY #$00 + BEQ .Apply + ASL + DEY + BNE .Loop2Head + .Apply: + ORA !consumable_checks, X + STA !consumable_checks, X + PLA + TAX + RTS + +GoToMostRecentWily: + LDA !controller_mirror + CMP !CONTROLLER_SELECT_START + BEQ .Default + LDA !last_wily + BNE .Store + .Default: + LDA #$08 ; wily stage 1 + .Store: + STA !current_stage + RTS + +StoreWilyStageCompletion: + LDA #$01 + STA !stage_completion, X + INC !current_stage + LDA !current_stage + STA !last_wily + RTS + +ReturnToGameOver: + LDA #$10 + STA !PpuControl_2000 + LDA #$06 + STA !PpuMask_2001 + JMP $C1BE ; specific code that loads game over + +MainLoopHook: + LDA !controller_mirror + CMP !CONTROLLER_ALL_BUTTON + BNE .Next + JMP ReturnToGameOver + .Next: + LDA !deathlink + CMP #$01 + BNE .Next2 + JMP $E5A8 ; this kills the Mega Man + .Next2: + print "Quickswap:", hex(realbase()) + LDA #$00 ; slot data, write in enable for quickswap + CMP #$01 + BNE .Finally + LDA !controller_flip + AND !CONTROLLER_SELECT + BEQ .Finally + JMP Quickswap + .Finally: + LDA !controller_flip + AND #$08 ; this is checking for menu + RTS + +StoreWilyProgress: + STA !current_stage + TXA + PHA + LDX !current_stage + LDA #$01 + STA !stage_completion, X + PLA + TAX + print "Get Equipped Music: ", hex(realbase()) + LDA #$17 + RTS + +KillMegaMan: + JSR $C051 ; this kills the mega man + LDA #$00 + STA $06C0 ; set HP to zero so client can actually detect he died + RTS + +Wily5Requirement: + LDA #$01 + LDX #$08 + LDY #$00 + .LoopHead: + BIT $BC + BEQ .Skip + INY + .Skip: + DEX + ASL + CPX #$00 + BNE .LoopHead + print "Wily 5 Requirement:", hex(realbase()) + CPY #$08 + BCS .SpawnTeleporter + JMP $8450 + .SpawnTeleporter: + LDA #$FF + STA $BC + LDA #$01 + STA $99 + JMP $8433 + +CleanWily5: + LDA #$00 + STA $BC + STA $99 + JMP $80AB + +LoadString: + STY $00 + ASL + ASL + ASL + ASL + TAY + LDA $DB + ADC #$00 + STA $C8 + LDA #$40 + STA $C9 + LDA #$F6 + CLC + ADC $C8 + STA $CA + LDA ($C9), Y + STA $03B6 + TYA + CLC + ADC #$01 + TAY + LDA $CA + ADC #$00 + STA $CA + LDA ($C9), Y + STA $03B7 + TYA + CLC + ADC #$01 + TAY + LDA $CA + ADC #$00 + STA $CA + STY $FE + LDA #$0E + STA $FD + .LoopHead: + JSR $BD34 + LDY $FE + CPY #$40 + BNE .NotEqual + LDA $0420 + BNE .Skip + .NotEqual: + LDA ($C9), Y + .Skip: + STA $03B8 + INC $47 + INC $03B7 + LDA $FE + CLC + ADC #$01 + STA $FE + LDA $CA + ADC #$00 + STA $CA + DEC $FD + BNE .LoopHead + LDY $00 + JSR $C0AB + RTS + +StageGetEquipped: + LDA !current_stage + LDX #$00 + BCS LoadGetEquipped +ItemGetEquipped: + LDX #$02 +LoadGetEquipped: + STX $DB + ASL + ASL + PHA + SEC + JSR LoadString + PLA + ADC #$00 + PHA + SEC + JSR LoadString + PLA + ADC #$00 + PHA + SEC + JSR LoadString + LDA #$00 + SEC + JSR $BD3E + PLA + ADC #$00 + SEC + JSR LoadString + RTS + +LoadItemsColor: + LDA #$7D + STA $FD + LDA $0420 + AND #$0F + ASL + SEC + ADC #$1A + STA $FF + RTS + +Energylink: + LSR $0420, X + print "Energylink: ", hex(realbase()) + LDA #$00 + BEQ .ApplyDrop + LDA $04E0, X + BEQ .ApplyDrop ; This is a stage pickup, and not an enemy drop + STY !energylink_packet + SEC + BCS .Return + .ApplyDrop: + STY $AD + .Return: + RTS + + +Quickswap: + LDX #$0F + .LoopHead: + LDA $0420, X + BMI .Return1 ; return if we have any weapon entities spawned + DEX + CPX #$01 + BNE .LoopHead + LDX !current_weapon + BNE .DoQuickswap + LDX #$00 + .DoQuickswap: + TYA + PHA + LDX !current_weapon + INX + CPX #$09 + BPL .Items + LDA #$01 + .Loop2Head: + DEX + BEQ .FoundTarget + ASL + CPX #$00 + BNE .Loop2Head + .FoundTarget: + LDX !current_weapon + INX + .Loop3Head: + PHA + AND !received_weapons + BNE .CanSwap + PLA + INX + CPX #$09 + BPL .Items + ASL + BNE .Loop3Head + .CanSwap: + PLA + SEC + BCS .ApplySwap + .Items: + TXA + PHA + SEC + SBC #$08 + TAX + LDA #$01 + .Loop4Head: + DEX + BEQ .CheckItem + ASL + CPX #$00 + BNE .Loop4Head + .CheckItem: + TAY + PLA + TAX + TYA + .Loop5Head: + PHA + AND !received_items + BNE .CanSwap + PLA + INX + ASL + BNE .Loop5Head + LDX #$00 + SEC + BCS .ApplySwap + .Return1: + RTS + .ApplySwap: ; $F408 on old rom + LDA #$0D + JSR !LOAD_BANK + ; this is a bunch of boiler plate to make the swap work + LDA $B5 + PHA + LDA $B6 + PHA + LDA $B7 + PHA + LDA $B8 + PHA + LDA $B9 + PHA + LDA $20 + PHA + LDA $1F + PHA + ;but wait, there's more + STX !current_weapon + JSR $CC6C + LDA $1A + PHA + LDX #$00 + .Loop6Head: + STX $FD + CLC + LDA $52 + ADC $957F, X + STA $08 + LDA $53 + ADC #$00 + STA $09 + LDA $08 + LSR $09 + ROR + LSR $09 + ROR + STA $08 + AND #$3F + STA $1A + CLC + LDA $09 + ADC #$85 + STA $09 + LDA #$00 + STA $1B + LDA $FD + CMP #$08 + BCS .Past8 + LDX $A9 + LDA $9664, X + TAY + CPX #$09 + BCC .LessThanNine + LDX #$00 + BEQ .Apply + .LessThanNine: + LDX #$05 + BNE .Apply + .Past8: + LDY #$90 + LDX #$00 + .Apply: + JSR $C760 + JSR $C0AB ; iirc this is loading graphics? + LDX $FD + INX + CPX #$0F + BNE .Loop6Head + STX $FD + LDY #$90 + LDX #$00 + JSR $C760 + JSR $D2ED + ; two sections redacted here, might need to look at what they actually do? + PLA + STA $1A + PLA + STA $1F + PLA + STA $20 + PLA + STA $B9 + PLA + STA $B8 + PLA + STA $B7 + PLA + STA $B6 + PLA + STA $B5 + LDA #$00 + STA $AC + STA $2C + STA $0680 + STA $06A0 + LDA #$1A + STA $0400 + LDA #$03 + STA $AA + LDA #$30 + JSR $C051 + .Finally: + LDA #$0E + JSR !LOAD_BANK + PLA + TAY + .Return: + RTS + +RefreshRBMTiles: + ; primarily just a copy of the startup RBM setup, we just do it again + ; can't jump to it as it leads into the main loop + LDA !rbm_strobe + BNE .Update + JMP .NoUpdate + .Update: + LDA #$00 + STA !rbm_strobe + LDA #$10 + STA $F7 + STA !PpuControl_2000 + LDA #$06 + STA $F8 + STA !PpuMask_2001 + JSR $847E + JSR $843C + LDX #$00 + LDA $8A + STA $01 + .TileLoop: + STX $00 + LSR $01 + BCC .SkipTile + LDA $8531,X + STA $09 + LDA $8539,X + STA $08 + LDX #$04 + LDA #$00 + .ClearBody: + LDA $09 + STA !PpuAddr_2006 + LDA $08 + STA !PpuAddr_2006 + LDY #$04 + LDA #$00 + .ClearLine: + STA !PpuData_2007 + DEY + BNE .ClearLine + CLC + LDA $08 + ADC #$20 + STA $08 + DEX + BNE .ClearBody + .SkipTile: + LDX $00 + INX + CPX #$08 + BNE .TileLoop + LDX #$1F + JSR $829E + JSR $8473 + LDX #$00 + LDA $8A + STA $02 + LDY #$00 + .SpriteLoop: + STX $01 + LSR $02 + BCS .SkipRBM + LDA $8605,X + STA $00 + LDA $85FD,X + TAX + .WriteSprite: + LDA $8541,X + STA $0200,Y + INY + INX + DEC $00 + BNE .WriteSprite + .SkipRBM: + LDX $01 + INX + CPX #$08 + BNE .SpriteLoop + JSR $A51D + LDA #$0C + JSR $C051 + LDA #$00 + STA $2A + STA $FD + JSR $C0AB + .NoUpdate: + LDA $1C + AND #$08 + RTS + +ClearRefresh: + LDA #$00 + STA !rbm_strobe + LDA #$10 + STA $F7 + RTS + +assert realbase() <= $03F650 ; This is the start of our text data, and we absolutely cannot go past this point (text takes too much room). + +%org($F640, $0F) +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" +db $25, $4B, "PLACEHOLDER_L1" +db $25, $6B, "PLACEHOLDER_L2" +db $25, $8B, "PLACEHOLDER_L3" +db $25, $EB, "PLACEHOLDER_PL" + +%org($FFB0, $0F) +db "MM2_BASEPATCH_ARCHI " \ No newline at end of file diff --git a/worlds/mm2/src/mm2font.dat b/worlds/mm2/src/mm2font.dat new file mode 100644 index 000000000000..4bf97ee42c69 Binary files /dev/null and b/worlds/mm2/src/mm2font.dat differ diff --git a/worlds/mm2/src/mm2titlefont.dat b/worlds/mm2/src/mm2titlefont.dat new file mode 100644 index 000000000000..cb0c9b13cfc2 Binary files /dev/null and b/worlds/mm2/src/mm2titlefont.dat differ diff --git a/worlds/mm2/test/__init__.py b/worlds/mm2/test/__init__.py new file mode 100644 index 000000000000..e712b0fe2ba6 --- /dev/null +++ b/worlds/mm2/test/__init__.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class MM2TestBase(WorldTestBase): + game = "Mega Man 2" diff --git a/worlds/mm2/test/test_access.py b/worlds/mm2/test/test_access.py new file mode 100644 index 000000000000..97ef5075a3cb --- /dev/null +++ b/worlds/mm2/test/test_access.py @@ -0,0 +1,47 @@ +from . import MM2TestBase +from ..locations import (quick_man_locations, heat_man_locations, wily_1_locations, wily_2_locations, + wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations, + energy_pickups, etank_1ups) +from ..names import * + + +class TestAccess(MM2TestBase): + options = { + "consumables": "all" + } + + def test_time_stopper(self) -> None: + """Optional based on Enable Lasers setting, confirm these are the locations affected""" + locations = [*quick_man_locations, *energy_pickups["Quick Man Stage"], *etank_1ups["Quick Man Stage"]] + items = [["Time Stopper"]] + self.assertAccessDependency(locations, items) + + def test_item_2(self) -> None: + """Optional based on Yoku Block setting, confirm these are the locations affected""" + locations = [*heat_man_locations, *etank_1ups["Heat Man Stage"]] + items = [["Item 2 - Rocket"]] + self.assertAccessDependency(locations, items, True) + + def test_any_item(self) -> None: + locations = [flash_man_c2, quick_man_c1, crash_man_c3] + items = [["Item 1 - Propeller"], ["Item 2 - Rocket"], ["Item 3 - Bouncy"]] + self.assertAccessDependency(locations, items, True) + locations = [metal_man_c2, metal_man_c3] + items = [["Item 1 - Propeller"], ["Item 2 - Rocket"]] + self.assertAccessDependency(locations, items, True) + + def test_all_items(self) -> None: + locations = [flash_man_c2, quick_man_c1, crash_man_c3, metal_man_c2, metal_man_c3, *heat_man_locations, + *etank_1ups["Heat Man Stage"], *wily_1_locations, *wily_2_locations, *wily_3_locations, + *wily_4_locations, *wily_5_locations, *wily_6_locations, *etank_1ups["Wily Stage 1"], + *etank_1ups["Wily Stage 2"], *etank_1ups["Wily Stage 3"], *etank_1ups["Wily Stage 4"], + *energy_pickups["Wily Stage 1"], *energy_pickups["Wily Stage 2"], *energy_pickups["Wily Stage 3"], + *energy_pickups["Wily Stage 4"]] + items = [["Item 1 - Propeller", "Item 2 - Rocket", "Item 3 - Bouncy"]] + self.assertAccessDependency(locations, items) + + def test_crash_bomber(self) -> None: + locations = [flash_man_c3, flash_man_c4, wily_2_c5, wily_2_c6, wily_3_c1, wily_3_c2, + wily_4, wily_stage_4] + items = [["Crash Bomber"]] + self.assertAccessDependency(locations, items) diff --git a/worlds/mm2/test/test_weakness.py b/worlds/mm2/test/test_weakness.py new file mode 100644 index 000000000000..c294ce5ac989 --- /dev/null +++ b/worlds/mm2/test/test_weakness.py @@ -0,0 +1,104 @@ +from math import ceil + +from . import MM2TestBase +from ..options import bosses + + +# Need to figure out how this test should work +def validate_wily_5(base: MM2TestBase) -> None: + world = base.multiworld.worlds[base.player] + weapon_damage = world.weapon_damage + weapon_costs = { + 0: 0, + 1: 10, + 2: 2, + 3: 3, + 4: 0.5, + 5: 0.125, + 6: 4, + 7: 0.25, + 8: 7, + } + boss_health = {boss: 0x1C if boss != 12 else 0x1C * 2 for boss in [*range(8), 12]} + weapon_energy = {key: float(0x1C) for key in weapon_costs} + weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage} + for boss in [*range(8), 12]} + flexibility = { + boss: ( + sum(damage_value > 0 for damage_value in + weapon_damages.values()) # Amount of weapons that hit this boss + * sum(weapon_damages.values()) # Overall damage that those weapons do + ) + for boss, weapon_damages in weapon_boss.items() if boss != 12 + } + flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value + used_weapons = {i: set() for i in [*range(8), 12]} + for boss in [*flexibility, 12]: + boss_damage = weapon_boss[boss] + weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in + boss_damage.items() if weapon_energy[weapon] > 0} + if boss_damage[8]: + boss_damage[8] = 1.75 * boss_damage[8] + if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight: + # We get exactly one use of Time Stopper during the rush + # So we want to make sure that use is absolutely needed + weapon_weight[8] = min(weapon_weight[8], 0.001) + while boss_health[boss] > 0: + if boss_damage[0] > 0: + boss_health[boss] = 0 # if we can buster, we should buster + continue + highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) + uses = weapon_energy[wp] // weapon_costs[wp] + used_weapons[boss].add(wp) + if int(uses * boss_damage[wp]) > boss_health[boss]: + used = ceil(boss_health[boss] / boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * used + boss_health[boss] = 0 + elif highest <= 0: + # we are out of weapons that can actually damage the boss + base.fail(f"Ran out of weapon energy to damage " + f"{next(name for name in bosses if bosses[name] == boss)}\n" + f"Seed: {base.multiworld.seed}\n" + f"Damage Table: {weapon_damage}") + else: + # drain the weapon and continue + boss_health[boss] -= int(uses * boss_damage[wp]) + weapon_energy[wp] -= weapon_costs[wp] * uses + weapon_weight.pop(wp) + + +class StrictWeaknessTests(MM2TestBase): + options = { + "strict_weakness": True, + "yoku_jumps": True, + "enable_lasers": True + } + + def test_that_every_boss_has_a_weakness(self) -> None: + world = self.multiworld.worlds[self.player] + weapon_damage = world.weapon_damage + for boss in range(14): + if not any(weapon_damage[weapon][boss] for weapon in range(9)): + self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}") + + def test_wily_5(self) -> None: + validate_wily_5(self) + + +class RandomStrictWeaknessTests(MM2TestBase): + options = { + "strict_weakness": True, + "random_weakness": "randomized", + "yoku_jumps": True, + "enable_lasers": True + } + + def test_that_every_boss_has_a_weakness(self) -> None: + world = self.multiworld.worlds[self.player] + weapon_damage = world.weapon_damage + for boss in range(14): + if not any(weapon_damage[weapon][boss] for weapon in range(9)): + self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}") + + def test_wily_5(self) -> None: + validate_wily_5(self) diff --git a/worlds/mm2/text.py b/worlds/mm2/text.py new file mode 100644 index 000000000000..7dda12ac0346 --- /dev/null +++ b/worlds/mm2/text.py @@ -0,0 +1,90 @@ +from typing import DefaultDict +from collections import defaultdict + +MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x6F, { + ' ': 0x40, + 'A': 0x41, + 'B': 0x42, + 'C': 0x43, + 'D': 0x44, + 'E': 0x45, + 'F': 0x46, + 'G': 0x47, + 'H': 0x48, + 'I': 0x49, + 'J': 0x4A, + 'K': 0x4B, + 'L': 0x4C, + 'M': 0x4D, + 'N': 0x4E, + 'O': 0x4F, + 'P': 0x50, + 'Q': 0x51, + 'R': 0x52, + 'S': 0x53, + 'T': 0x54, + 'U': 0x55, + 'V': 0x56, + 'W': 0x57, + 'X': 0x58, + 'Y': 0x59, + 'Z': 0x5A, + # 0x5B is the small r in Dr Light + '.': 0x5C, + ',': 0x5D, + '\'': 0x5E, + '!': 0x5F, + '(': 0x60, + ')': 0x61, + '#': 0x62, + '$': 0x63, + '%': 0x64, + '&': 0x65, + '*': 0x66, + '+': 0x67, + '/': 0x68, + '\\': 0x69, + ':': 0x6A, + ';': 0x6B, + '<': 0x6C, + '>': 0x6D, + '=': 0x6E, + '?': 0x6F, + '@': 0x70, + '[': 0x71, + ']': 0x72, + '^': 0x73, + '_': 0x74, + '`': 0x75, + '{': 0x76, + '}': 0x77, + '|': 0x78, + '~': 0x79, + '\"': 0x92, + '-': 0x94, + '0': 0xA0, + '1': 0xA1, + '2': 0xA2, + '3': 0xA3, + '4': 0xA4, + '5': 0xA5, + '6': 0xA6, + '7': 0xA7, + '8': 0xA8, + '9': 0xA9, +}) + + +class MM2TextEntry: + def __init__(self, text: str = "", coords: int = 0x0B): + self.target_area: int = 0x25 # don't change + self.coords: int = coords # 0xYX, Y can only be increments of 0x20 + self.text: str = text + + def resolve(self) -> bytes: + data = bytearray() + data.append(self.target_area) + data.append(self.coords) + data.extend([MM2_WEAPON_ENCODING[x] for x in self.text.upper()]) + data.extend([0x40] * (14 - len(self.text))) + return bytes(data) diff --git a/worlds/mmbn3/Items.py b/worlds/mmbn3/Items.py index 2e249ce79e8c..30ec311ecbe2 100644 --- a/worlds/mmbn3/Items.py +++ b/worlds/mmbn3/Items.py @@ -171,7 +171,7 @@ class MMBN3Item(Item): ItemData(0xB31063, ItemName.SandStage_C, ItemClassification.filler, ItemType.Chip, 182, chip_code('C')), ItemData(0xB31064, ItemName.SideGun_S, ItemClassification.filler, ItemType.Chip, 12, chip_code('S')), ItemData(0xB31065, ItemName.Slasher_B, ItemClassification.useful, ItemType.Chip, 43, chip_code('B')), - ItemData(0xB31066, ItemName.SloGuage_star, ItemClassification.filler, ItemType.Chip, 157, chip_code('*')), + ItemData(0xB31066, ItemName.SloGauge_star, ItemClassification.filler, ItemType.Chip, 157, chip_code('*')), ItemData(0xB31067, ItemName.Snake_D, ItemClassification.useful, ItemType.Chip, 131, chip_code('D')), ItemData(0xB31068, ItemName.Snctuary_C, ItemClassification.useful, ItemType.Chip, 184, chip_code('C')), ItemData(0xB31069, ItemName.Spreader_star, ItemClassification.useful, ItemType.Chip, 13, chip_code('*')), diff --git a/worlds/mmbn3/Names/ItemName.py b/worlds/mmbn3/Names/ItemName.py index 441bdc591c51..677eff22b353 100644 --- a/worlds/mmbn3/Names/ItemName.py +++ b/worlds/mmbn3/Names/ItemName.py @@ -72,7 +72,7 @@ class ItemName(): SandStage_C = "SandStage C" SideGun_S = "SideGun S" Slasher_B = "Slasher B" - SloGuage_star = "SloGuage *" + SloGauge_star = "SloGauge *" Snake_D = "Snake D" Snctuary_C = "Snctuary C" Spreader_star = "Spreader *" @@ -235,4 +235,4 @@ class ItemName(): RegUP3 = "RegUP3" SubMem = "SubMem" - Victory = "Victory" \ No newline at end of file + Victory = "Victory" 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/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py index 576a106df7cc..9e8c9214a1dd 100644 --- a/worlds/musedash/MuseDashCollection.py +++ b/worlds/musedash/MuseDashCollection.py @@ -46,6 +46,8 @@ class MuseDashCollections: "CHAOS Glitch", "FM 17314 SUGAR RADIO", "Yume Ou Mono Yo Secret", + "Echo over you... Secret", + "Tsukuyomi Ni Naru Replaced", ] album_items: Dict[str, AlbumData] = {} diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 6f48d6af9fdd..d913449ed540 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -31,7 +31,7 @@ Blackest Luxury Car|0-18|Default Music|True|3|6|8| Medicine of Sing|0-19|Default Music|False|3|6|8| irregulyze|0-20|Default Music|True|3|6|8| I don't care about Christmas though|0-47|Default Music|False|4|6|8| -Imaginary World|0-21|Default Music|True|4|6|8| +Imaginary World|0-21|Default Music|True|4|6|8|10 Dysthymia|0-22|Default Music|True|4|7|9| From the New World|0-42|Default Music|False|2|5|7| NISEGAO|0-33|Default Music|True|4|7|9| @@ -266,7 +266,7 @@ Medusa|31-1|Happy Otaku Pack Vol.11|False|4|6|8|10 Final Step!|31-2|Happy Otaku Pack Vol.11|False|5|7|10| MAGENTA POTION|31-3|Happy Otaku Pack Vol.11|False|4|7|9| Cross Ray|31-4|Happy Otaku Pack Vol.11|False|3|6|9| -Square Lake|31-5|Happy Otaku Pack Vol.11|True|6|8|9|11 +Square Lake|31-5|Happy Otaku Pack Vol.11|False|6|8|9|11 Girly Cupid|30-0|Cute Is Everything Vol.6|False|3|6|8| sheep in the light|30-1|Cute Is Everything Vol.6|False|2|5|8| Breaker city|30-2|Cute Is Everything Vol.6|False|4|6|9| @@ -353,7 +353,7 @@ Re End of a Dream|16-1|Give Up TREATMENT Vol.6|False|5|8|11| Etude -Storm-|16-2|Give Up TREATMENT Vol.6|True|6|8|10| Unlimited Katharsis|16-3|Give Up TREATMENT Vol.6|False|4|6|10| Magic Knight Girl|16-4|Give Up TREATMENT Vol.6|False|4|7|9| -Eeliaas|16-5|Give Up TREATMENT Vol.6|True|6|9|11| +Eeliaas|16-5|Give Up TREATMENT Vol.6|False|6|9|11| Magic Spell|15-0|Cute Is Everything Vol.3|True|2|5|7| Colorful Star, Colored Drawing, Travel Poem|15-1|Cute Is Everything Vol.3|False|3|4|6| Satell Knight|15-2|Cute Is Everything Vol.3|False|3|6|8| @@ -396,7 +396,7 @@ Chronomia|9-2|Happy Otaku Pack Vol.4|False|5|7|10| Dandelion's Daydream|9-3|Happy Otaku Pack Vol.4|True|5|7|8| Lorikeet Flat design|9-4|Happy Otaku Pack Vol.4|True|5|7|10| GOODRAGE|9-5|Happy Otaku Pack Vol.4|False|6|9|11| -Altale|8-0|Give Up TREATMENT Vol.3|False|3|5|7| +Altale|8-0|Give Up TREATMENT Vol.3|False|3|5|7|10 Brain Power|8-1|Give Up TREATMENT Vol.3|False|4|7|10| Berry Go!!|8-2|Give Up TREATMENT Vol.3|False|3|6|9| Sweet* Witch* Girl*|8-3|Give Up TREATMENT Vol.3|False|6|8|10|? @@ -553,7 +553,7 @@ NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10| Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10| Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10| World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11 -Tsukuyomi Ni Naru|74-2|CHUNITHM COURSE MUSE|False|5|7|9| +Tsukuyomi Ni Naru Replaced|74-2|CHUNITHM COURSE MUSE|True|5|7|9| The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11 Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11 Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12 @@ -567,3 +567,31 @@ SUPERHERO|75-0|Novice Rider Pack|False|2|4|7| Highway_Summer|75-1|Novice Rider Pack|True|2|4|6| Mx. Black Box|75-2|Novice Rider Pack|True|5|7|9| Sweet Encounter|75-3|Novice Rider Pack|True|2|4|7| +Echo over you... Secret|0-55|Default Music|False|6|8|10| +Echo over you...|0-56|Default Music|False|1|4|0| +Tsukuyomi Ni Naru|74-6|CHUNITHM COURSE MUSE|True|5|8|10| +disco light|76-0|MUSE RADIO FM105|True|5|7|9| +room light feat.chancylemon|76-1|MUSE RADIO FM105|True|3|5|7| +Invisible|76-2|MUSE RADIO FM105|True|3|5|8| +Christmas Season-LLABB|76-3|MUSE RADIO FM105|True|1|4|7| +Hyouryu|77-0|Let's Rhythm Jam!|False|6|8|10| +The Whole Rest|77-1|Let's Rhythm Jam!|False|5|8|10|11 +Hydra|77-2|Let's Rhythm Jam!|False|4|7|11| +Pastel Lines|77-3|Let's Rhythm Jam!|False|3|6|9| +LINK x LIN#S|77-4|Let's Rhythm Jam!|False|3|6|9| +Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|11| +Eve Avenir|78-0|Endless Pirouette|True|6|8|10| +Silverstring|78-1|Endless Pirouette|True|5|7|10| +Melusia|78-2|Endless Pirouette|False|5|7|10|11 +Devil's Castle|78-3|Endless Pirouette|True|4|7|10| +Abatement|78-4|Endless Pirouette|True|6|8|10|11 +Azalea|78-5|Endless Pirouette|False|4|8|10| +Brightly World|78-6|Endless Pirouette|True|6|8|10| +We'll meet in every world ***|78-7|Endless Pirouette|True|7|9|11| +Collapsar|78-8|Endless Pirouette|True|7|9|10|11 +Parousia|78-9|Endless Pirouette|False|6|8|10| +Gunners in the Rain|79-0|Ensemble Arcanum|False|5|8|10| +Halzion|79-1|Ensemble Arcanum|False|2|5|8| +SHOWTIME!!|79-2|Ensemble Arcanum|False|6|8|10| +Achromic Riddle|79-3|Ensemble Arcanum|False|6|8|10|11 +karanosu|79-4|Ensemble Arcanum|False|3|6|8| diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 7164aa3e1362..b8c969c39b0f 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -11,7 +11,6 @@ class DLCMusicPacks(OptionSet): Note: The [Just As Planned] DLC contains all [Muse Plus] songs. """ display_name = "DLC Packs" - default = {} valid_keys = [dlc for dlc in MuseDashCollections.DLC] @@ -39,7 +38,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 534 # Note will probably not reach this high if any other settings are done. + range_end = 600 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" @@ -142,7 +141,6 @@ class ChosenTraps(OptionSet): Note: SFX traps are only available if [Just as Planned] DLC songs are enabled. """ display_name = "Chosen Traps" - default = {} valid_keys = {trap for trap in MuseDashCollections.trap_items.keys()} diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index ab3a4819fc48..be2eec2f87b8 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -183,7 +183,7 @@ def create_item(self, name: str) -> Item: if album: return MuseDashSongItem(name, self.player, album) - song = self.md_collection.song_items.get(name) + song = self.md_collection.song_items[name] return MuseDashSongItem(name, self.player, song) def get_filler_item_name(self) -> str: diff --git a/worlds/noita/items.py b/worlds/noita/items.py index 6b662fbee692..1cb7d9601386 100644 --- a/worlds/noita/items.py +++ b/worlds/noita/items.py @@ -100,13 +100,13 @@ def create_all_items(world: NoitaWorld) -> None: "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1), "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), "Kantele": ItemData(110012, "Wands", ItemClassification.useful), - "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), - "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), - "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1), - "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1), - "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1), - "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1), - "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1), + "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1), "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), 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 bbdc30490c18..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_events(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))} + all_state.sweep_for_advancements(locations=ootworld.get_locations()) + 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,18 +785,18 @@ 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() none_state = none_state_orig.copy() - all_state.sweep_for_events(locations=ootworld.get_locations()) - none_state.sweep_for_events(locations=ootworld.get_locations()) + all_state.sweep_for_advancements(locations=ootworld.get_locations()) + none_state.sweep_for_advancements(locations=ootworld.get_locations()) if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() - time_travel_state.collect(ootworld.create_item('Time Travel'), event=True) + time_travel_state.collect(ootworld.create_item('Time Travel'), prevent_sweep=True) time_travel_state._oot_update_age_reachable_regions(player) # Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age @@ -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..797b276b766c 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, OptionSet, DeathLink, PlandoConnections, \ + PerGameCommonOptions, OptionGroup from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -1270,7 +1272,7 @@ class SfxOcarina(Choice): } -class LogicTricks(OptionList): +class LogicTricks(OptionSet): """Set various tricks for logic in Ocarina of Time. Format as a comma-separated list of "nice" names: ["Fewer Tunic Requirements", "Hidden Grottos without Stone of Agony"]. A full list of supported tricks can be found at: @@ -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 529411f6fc2c..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': @@ -228,7 +233,7 @@ def set_shop_rules(ootworld): def set_entrances_based_rules(ootworld): all_state = ootworld.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_locations()) + all_state.sweep_for_advancements(locations=ootworld.get_locations()) for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): # If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these 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 89f10a5a2da0..975902ae6e64 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 @@ -182,6 +184,10 @@ class OOTWorld(World): "Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)", "Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)", "Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"}, + + # aliases + "Longshot": {"Progressive Hookshot"}, # fuzzy hinting thought Longshot was Slingshot + "Hookshot": {"Progressive Hookshot"}, # for consistency, mostly } location_name_groups = build_location_name_groups() @@ -195,15 +201,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 +229,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 +289,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 +303,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 +317,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 +339,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 +399,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 +411,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 +431,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 +582,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 +613,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 +630,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 +638,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 +673,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 +784,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 @@ -847,7 +853,7 @@ def generate_basic(self): # mostly killing locations that shouldn't exist by se # Make sure to only kill actual internal events, not in-game "events" all_state = self.get_state_with_complete_itempool() all_locations = self.get_locations() - all_state.sweep_for_events(locations=all_locations) + all_state.sweep_for_advancements(locations=all_locations) reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if (loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable] @@ -875,19 +881,19 @@ def prefill_state(base_state): state = base_state.copy() for item in self.get_pre_fill_items(): self.collect(state, item) - state.sweep_for_events(locations=self.get_locations()) + state.sweep_for_advancements(locations=self.get_locations()) return 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) for item in self.itempool: self.collect(state, item) - state.sweep_for_events(locations=self.get_locations()) + state.sweep_for_advancements(locations=self.get_locations()) # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] @@ -910,7 +916,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 +929,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 +970,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 +1002,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 +1034,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 +1080,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 +1098,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 +1307,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 +1320,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 @@ -1337,23 +1348,9 @@ def get_shuffled_entrances(self, type=None, only_primary=False): def get_locations(self): return self.multiworld.get_locations(self.player) - def get_location(self, location): - return self.multiworld.get_location(location, self.player) - - def get_region(self, region_name): - try: - return self._regions_cache[region_name] - except KeyError: - ret = self.multiworld.get_region(region_name, self.player) - self._regions_cache[region_name] = ret - return ret - def get_entrances(self): return self.multiworld.get_entrances(self.player) - def get_entrance(self, entrance): - return self.multiworld.get_entrance(entrance, self.player) - def is_major_item(self, item: OOTItem): if item.type == 'Token': return self.bridge == 'tokens' or self.lacs_condition == 'tokens' @@ -1388,13 +1385,13 @@ def get_state_with_complete_itempool(self): self.multiworld.worlds[item.player].collect(all_state, item) # If free_scarecrow give Scarecrow Song if self.free_scarecrow: - all_state.collect(self.create_item("Scarecrow Song"), event=True) - all_state.stale[self.player] = True + all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=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/Items.py b/worlds/osrs/Items.py new file mode 100644 index 000000000000..0679c964e772 --- /dev/null +++ b/worlds/osrs/Items.py @@ -0,0 +1,85 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemNames + + +class ItemRow(typing.NamedTuple): + name: str + amount: int + progression: ItemClassification + + +class OSRSItem(Item): + game: str = "Old School Runescape" + + +QP_Items: typing.List[str] = [ + ItemNames.QP_Cooks_Assistant, + ItemNames.QP_Demon_Slayer, + ItemNames.QP_Restless_Ghost, + ItemNames.QP_Romeo_Juliet, + ItemNames.QP_Sheep_Shearer, + ItemNames.QP_Shield_of_Arrav, + ItemNames.QP_Ernest_the_Chicken, + ItemNames.QP_Vampyre_Slayer, + ItemNames.QP_Imp_Catcher, + ItemNames.QP_Prince_Ali_Rescue, + ItemNames.QP_Dorics_Quest, + ItemNames.QP_Black_Knights_Fortress, + ItemNames.QP_Witchs_Potion, + ItemNames.QP_Knights_Sword, + ItemNames.QP_Goblin_Diplomacy, + ItemNames.QP_Pirates_Treasure, + ItemNames.QP_Rune_Mysteries, + ItemNames.QP_Misthalin_Mystery, + ItemNames.QP_Corsair_Curse, + ItemNames.QP_X_Marks_the_Spot, + ItemNames.QP_Below_Ice_Mountain +] + +starting_area_dict: typing.Dict[int, str] = { + 0: ItemNames.Lumbridge, + 1: ItemNames.Al_Kharid, + 2: ItemNames.Central_Varrock, + 3: ItemNames.West_Varrock, + 4: ItemNames.Edgeville, + 5: ItemNames.Falador, + 6: ItemNames.Draynor_Village, + 7: ItemNames.Wilderness, +} + +chunksanity_starting_chunks: typing.List[str] = [ + ItemNames.Lumbridge, + ItemNames.Lumbridge_Swamp, + ItemNames.Lumbridge_Farms, + ItemNames.HAM_Hideout, + ItemNames.Draynor_Village, + ItemNames.Draynor_Manor, + ItemNames.Wizards_Tower, + ItemNames.Al_Kharid, + ItemNames.Citharede_Abbey, + ItemNames.South_Of_Varrock, + ItemNames.Central_Varrock, + ItemNames.Varrock_Palace, + ItemNames.East_Of_Varrock, + ItemNames.West_Varrock, + ItemNames.Edgeville, + ItemNames.Barbarian_Village, + ItemNames.Monastery, + ItemNames.Ice_Mountain, + ItemNames.Dwarven_Mines, + ItemNames.Falador, + ItemNames.Falador_Farm, + ItemNames.Crafting_Guild, + ItemNames.Rimmington, + ItemNames.Port_Sarim, + ItemNames.Mudskipper_Point, + ItemNames.Wilderness +] + +# Some starting areas contain multiple regions, so if that area is rolled for Chunksanity, we need to map it to one +chunksanity_special_region_names: typing.Dict[str, str] = { + ItemNames.Lumbridge_Farms: 'Lumbridge Farms East', + ItemNames.Crafting_Guild: 'Crafting Guild Outskirts', +} diff --git a/worlds/osrs/Locations.py b/worlds/osrs/Locations.py new file mode 100644 index 000000000000..b5827d60f2fe --- /dev/null +++ b/worlds/osrs/Locations.py @@ -0,0 +1,21 @@ +import typing + +from BaseClasses import Location + + +class SkillRequirement(typing.NamedTuple): + skill: str + level: int + + +class LocationRow(typing.NamedTuple): + name: str + category: str + regions: typing.List[str] + skills: typing.List[SkillRequirement] + items: typing.List[str] + qp: int + + +class OSRSLocation(Location): + game: str = "Old School Runescape" diff --git a/worlds/osrs/LogicCSV/LogicCSVToPython.py b/worlds/osrs/LogicCSV/LogicCSVToPython.py new file mode 100644 index 000000000000..ed8bd8172a01 --- /dev/null +++ b/worlds/osrs/LogicCSV/LogicCSVToPython.py @@ -0,0 +1,144 @@ +""" +This is a utility file that converts logic in the form of CSV files into Python files that can be imported and used +directly by the world implementation. Whenever the logic files are updated, this script should be run to re-generate +the python files containing the data. +""" +import requests + +# The CSVs are updated at this repository to be shared between generator and client. +data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/" +# The Github tag of the CSVs this was generated with +data_csv_tag = "v1.5" + +if __name__ == "__main__": + import sys + import os + import csv + import typing + + # makes this module runnable from its world folder. Shamelessly stolen from Subnautica + sys.path.remove(os.path.dirname(__file__)) + new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.chdir(new_home) + sys.path.append(new_home) + + + def load_location_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as locPyFile: + locPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + locPyFile.write("from ..Locations import LocationRow, SkillRequirement\n") + locPyFile.write("\n") + locPyFile.write("location_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req: + locations_reader = csv.reader(req.text.splitlines()) + for row in locations_reader: + row_line = "LocationRow(" + row_line += str_format(row[0]) + row_line += str_format(row[1].lower()) + + region_strings = row[2].split(", ") if row[2] else [] + row_line += f"{str_list_to_py(region_strings)}, " + + skill_strings = row[3].split(", ") + row_line += "[" + if skill_strings: + split_skills = [skill.split(" ") for skill in skill_strings if skill != ""] + if split_skills: + for split in split_skills: + row_line += f"SkillRequirement('{split[0]}', {split[1]}), " + row_line += "], " + + item_strings = row[4].split(", ") if row[4] else [] + row_line += f"{str_list_to_py(item_strings)}, " + row_line += f"{row[5]})" if row[5] != "" else "0)" + locPyFile.write(f"\t{row_line},\n") + locPyFile.write("]\n") + + def load_region_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as regPyFile: + regPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + regPyFile.write("from ..Regions import RegionRow\n") + regPyFile.write("\n") + regPyFile.write("region_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req: + regions_reader = csv.reader(req.text.splitlines()) + for row in regions_reader: + row_line = "RegionRow(" + row_line += str_format(row[0]) + row_line += str_format(row[1]) + connections = row[2].replace("'", "\\'") + row_line += f"{str_list_to_py(connections.split(', '))}, " + resources = row[3].replace("'", "\\'") + row_line += f"{str_list_to_py(resources.split(', '))})" + regPyFile.write(f"\t{row_line},\n") + regPyFile.write("]\n") + + def load_resource_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as resPyFile: + resPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + resPyFile.write("from ..Regions import ResourceRow\n") + resPyFile.write("\n") + resPyFile.write("resource_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req: + resource_reader = csv.reader(req.text.splitlines()) + for row in resource_reader: + name = row[0].replace("'", "\\'") + row_line = f"ResourceRow('{name}')" + resPyFile.write(f"\t{row_line},\n") + resPyFile.write("]\n") + + + def load_item_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "items_generated.py"), 'w+') as itemPyfile: + itemPyfile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + itemPyfile.write("from BaseClasses import ItemClassification\n") + itemPyfile.write("from ..Items import ItemRow\n") + itemPyfile.write("\n") + itemPyfile.write("item_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req: + item_reader = csv.reader(req.text.splitlines()) + for row in item_reader: + row_line = "ItemRow(" + row_line += str_format(row[0]) + row_line += f"{row[1]}, " + + row_line += f"ItemClassification.{row[2]})" + + itemPyfile.write(f"\t{row_line},\n") + itemPyfile.write("]\n") + + + def str_format(s) -> str: + ret_str = s.replace("'", "\\'") + return f"'{ret_str}', " + + + def str_list_to_py(str_list) -> str: + ret_str = "[" + for s in str_list: + ret_str += f"'{s}', " + ret_str += "]" + return ret_str + + + + load_location_csv() + print("Generated locations py") + load_region_csv() + print("Generated regions py") + load_resource_csv() + print("Generated resource py") + load_item_csv() + print("Generated item py") diff --git a/worlds/osrs/LogicCSV/items_generated.py b/worlds/osrs/LogicCSV/items_generated.py new file mode 100644 index 000000000000..b5e610a6e3ab --- /dev/null +++ b/worlds/osrs/LogicCSV/items_generated.py @@ -0,0 +1,43 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from BaseClasses import ItemClassification +from ..Items import ItemRow + +item_rows = [ + ItemRow('Area: Lumbridge', 1, ItemClassification.progression), + ItemRow('Area: Lumbridge Swamp', 1, ItemClassification.progression), + ItemRow('Area: HAM Hideout', 1, ItemClassification.progression), + ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression), + ItemRow('Area: South of Varrock', 1, ItemClassification.progression), + ItemRow('Area: East Varrock', 1, ItemClassification.progression), + ItemRow('Area: Central Varrock', 1, ItemClassification.progression), + ItemRow('Area: Varrock Palace', 1, ItemClassification.progression), + ItemRow('Area: West Varrock', 1, ItemClassification.progression), + ItemRow('Area: Edgeville', 1, ItemClassification.progression), + ItemRow('Area: Barbarian Village', 1, ItemClassification.progression), + ItemRow('Area: Draynor Manor', 1, ItemClassification.progression), + ItemRow('Area: Falador', 1, ItemClassification.progression), + ItemRow('Area: Dwarven Mines', 1, ItemClassification.progression), + ItemRow('Area: Ice Mountain', 1, ItemClassification.progression), + ItemRow('Area: Monastery', 1, ItemClassification.progression), + ItemRow('Area: Falador Farms', 1, ItemClassification.progression), + ItemRow('Area: Port Sarim', 1, ItemClassification.progression), + ItemRow('Area: Mudskipper Point', 1, ItemClassification.progression), + ItemRow('Area: Karamja', 1, ItemClassification.progression), + ItemRow('Area: Crandor', 1, ItemClassification.progression), + ItemRow('Area: Rimmington', 1, ItemClassification.progression), + ItemRow('Area: Crafting Guild', 1, ItemClassification.progression), + ItemRow('Area: Draynor Village', 1, ItemClassification.progression), + ItemRow('Area: Wizard Tower', 1, ItemClassification.progression), + ItemRow('Area: Corsair Cove', 1, ItemClassification.progression), + ItemRow('Area: Al Kharid', 1, ItemClassification.progression), + ItemRow('Area: Citharede Abbey', 1, ItemClassification.progression), + ItemRow('Area: Wilderness', 1, ItemClassification.progression), + ItemRow('Progressive Armor', 6, ItemClassification.progression), + ItemRow('Progressive Weapons', 6, ItemClassification.progression), + ItemRow('Progressive Tools', 6, ItemClassification.useful), + ItemRow('Progressive Ranged Weapons', 3, ItemClassification.useful), + ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful), + ItemRow('Progressive Magic', 2, ItemClassification.useful), +] diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py new file mode 100644 index 000000000000..2d617a7038fe --- /dev/null +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -0,0 +1,127 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Locations import LocationRow, SkillRequirement + +location_rows = [ + LocationRow('Quest: Cook\'s Assistant', 'quest', ['Lumbridge', 'Wheat', 'Windmill', 'Egg', 'Milk', ], [], [], 0), + LocationRow('Quest: Demon Slayer', 'quest', ['Central Varrock', 'Varrock Palace', 'Wizard Tower', 'South of Varrock', ], [], [], 0), + LocationRow('Quest: The Restless Ghost', 'quest', ['Lumbridge', 'Lumbridge Swamp', 'Wizard Tower', ], [], [], 0), + LocationRow('Quest: Romeo & Juliet', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0), + LocationRow('Quest: Sheep Shearer', 'quest', ['Lumbridge Farms West', 'Spinning Wheel', ], [], [], 0), + LocationRow('Quest: Shield of Arrav', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0), + LocationRow('Quest: Ernest the Chicken', 'quest', ['Draynor Manor', ], [], [], 0), + LocationRow('Quest: Vampyre Slayer', 'quest', ['Draynor Village', 'Central Varrock', 'Draynor Manor', ], [], [], 0), + LocationRow('Quest: Imp Catcher', 'quest', ['Wizard Tower', 'Imps', ], [], [], 0), + LocationRow('Quest: Prince Ali Rescue', 'quest', ['Al Kharid', 'Central Varrock', 'Bronze Ores', 'Clay Ore', 'Sheep', 'Spinning Wheel', 'Draynor Village', ], [], [], 0), + LocationRow('Quest: Doric\'s Quest', 'quest', ['Dwarven Mountain Pass', 'Clay Ore', 'Iron Ore', 'Bronze Ores', ], [SkillRequirement('Mining', 15), ], [], 0), + LocationRow('Quest: Black Knights\' Fortress', 'quest', ['Dwarven Mines', 'Falador', 'Monastery', 'Ice Mountain', 'Falador Farms', ], [], ['Progressive Armor', ], 12), + LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0), + LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0), + LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0), + LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', ], [], [], 0), + LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0), + LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0), + LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0), + LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0), + LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16), + LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32), + LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0), + LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2), + LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6), + LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0), + LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0), + LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 2), + LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6), + LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0), + LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0), + LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0), + LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0), + LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0), + LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0), + LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4), + LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8), + LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0), + LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0), + LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0), + LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2), + LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6), + LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0), + LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0), + LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2), + LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6), + LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0), + LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0), + LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2), + LocationRow('Catch a Lobster', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 40), ], [], 6), + LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), + LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), + LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), + LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2), + LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), + LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), + LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0), + LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), + LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), + LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0), + LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0), + LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0), + LocationRow('Kill a Barbarian', 'combat', ['Barbarian', ], [SkillRequirement('Combat', 10), ], [], 0), + LocationRow('Kill a Giant Frog', 'combat', ['Lumbridge Swamp', ], [SkillRequirement('Combat', 13), ], [], 0), + LocationRow('Kill a Zombie', 'combat', ['Zombie', ], [SkillRequirement('Combat', 13), ], [], 0), + LocationRow('Kill a Guard', 'combat', ['Guard', ], [SkillRequirement('Combat', 21), ], [], 0), + LocationRow('Kill a Hill Giant', 'combat', ['Hill Giant', ], [SkillRequirement('Combat', 28), ], [], 2), + LocationRow('Kill a Deadly Red Spider', 'combat', ['Deadly Red Spider', ], [SkillRequirement('Combat', 34), ], [], 2), + LocationRow('Kill a Moss Giant', 'combat', ['Moss Giant', ], [SkillRequirement('Combat', 42), ], [], 2), + LocationRow('Kill a Catablepon', 'combat', ['Barbarian Village', ], [SkillRequirement('Combat', 49), ], [], 4), + LocationRow('Kill an Ice Giant', 'combat', ['Ice Giant', ], [SkillRequirement('Combat', 53), ], [], 4), + LocationRow('Kill a Lesser Demon', 'combat', ['Lesser Demon', ], [SkillRequirement('Combat', 82), ], [], 8), + LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8), + LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28), + LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28), + LocationRow('Total XP 5,000', 'general', [], [], [], 0), + LocationRow('Combat Level 5', 'general', [], [], [], 0), + LocationRow('Total XP 10,000', 'general', [], [], [], 0), + LocationRow('Total Level 50', 'general', [], [], [], 0), + LocationRow('Total XP 25,000', 'general', [], [], [], 0), + LocationRow('Total Level 100', 'general', [], [], [], 0), + LocationRow('Total XP 50,000', 'general', [], [], [], 0), + LocationRow('Combat Level 15', 'general', [], [], [], 0), + LocationRow('Total Level 150', 'general', [], [], [], 2), + LocationRow('Total XP 75,000', 'general', [], [], [], 2), + LocationRow('Combat Level 25', 'general', [], [], [], 2), + LocationRow('Total XP 100,000', 'general', [], [], [], 6), + LocationRow('Total Level 200', 'general', [], [], [], 6), + LocationRow('Total XP 125,000', 'general', [], [], [], 6), + LocationRow('Combat Level 30', 'general', [], [], [], 10), + LocationRow('Total Level 250', 'general', [], [], [], 10), + LocationRow('Total XP 150,000', 'general', [], [], [], 10), + LocationRow('Total Level 300', 'general', [], [], [], 16), + LocationRow('Combat Level 40', 'general', [], [], [], 16), + LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0), + LocationRow('Points: Demon Slayer', 'points', [], [], [], 0), + LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0), + LocationRow('Points: Romeo & Juliet', 'points', [], [], [], 0), + LocationRow('Points: Sheep Shearer', 'points', [], [], [], 0), + LocationRow('Points: Shield of Arrav', 'points', [], [], [], 0), + LocationRow('Points: Ernest the Chicken', 'points', [], [], [], 0), + LocationRow('Points: Vampyre Slayer', 'points', [], [], [], 0), + LocationRow('Points: Imp Catcher', 'points', [], [], [], 0), + LocationRow('Points: Prince Ali Rescue', 'points', [], [], [], 0), + LocationRow('Points: Doric\'s Quest', 'points', [], [], [], 0), + LocationRow('Points: Black Knights\' Fortress', 'points', [], [], [], 0), + LocationRow('Points: Witch\'s Potion', 'points', [], [], [], 0), + LocationRow('Points: The Knight\'s Sword', 'points', [], [], [], 0), + LocationRow('Points: Goblin Diplomacy', 'points', [], [], [], 0), + LocationRow('Points: Pirate\'s Treasure', 'points', [], [], [], 0), + LocationRow('Points: Rune Mysteries', 'points', [], [], [], 0), + LocationRow('Points: Misthalin Mystery', 'points', [], [], [], 0), + LocationRow('Points: The Corsair Curse', 'points', [], [], [], 0), + LocationRow('Points: X Marks the Spot', 'points', [], [], [], 0), + LocationRow('Points: Below Ice Mountain', 'points', [], [], [], 0), +] diff --git a/worlds/osrs/LogicCSV/regions_generated.py b/worlds/osrs/LogicCSV/regions_generated.py new file mode 100644 index 000000000000..87b3747d938e --- /dev/null +++ b/worlds/osrs/LogicCSV/regions_generated.py @@ -0,0 +1,47 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Regions import RegionRow + +region_rows = [ + RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', ]), + RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', ]), + RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]), + RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', ]), + RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]), + RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]), + RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'East Varrock', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', ]), + RegionRow('East Varrock', 'Area: East Varrock', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', ]), + RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'East Varrock', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', ]), + RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'East Varrock', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]), + RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]), + RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', ]), + RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]), + RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]), + RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]), + RegionRow('Falador East Outskirts', 'Area: Falador', ['Dwarven Mountain Pass', 'Draynor Manor Outskirts', 'Falador Farms', ], ['', ]), + RegionRow('Dwarven Mountain Pass', 'Area: Dwarven Mines', ['Goblin Village', 'Monastery', 'Barbarian Village', 'Falador East Outskirts', 'Falador', ], ['Anvil*', 'Wheat', ]), + RegionRow('Dwarven Mines', 'Area: Dwarven Mines', ['Monastery', 'Ice Mountain', 'Falador', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Gold Ore', 'Anvil', 'Pie Dish', 'Clay Ore', ]), + RegionRow('Goblin Village', 'Area: Ice Mountain', ['Wilderness', 'Dwarven Mountain Pass', ], ['Meat', ]), + RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]), + RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]), + RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]), + RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', ]), + RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', ]), + RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]), + RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]), + RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', ]), + RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]), + RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]), + RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['', ]), + RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', ]), + RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]), + RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]), + RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]), + RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', ]), + RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]), + RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('Wilderness', 'Area: Wilderness', ['East Varrock', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]), +] diff --git a/worlds/osrs/LogicCSV/resources_generated.py b/worlds/osrs/LogicCSV/resources_generated.py new file mode 100644 index 000000000000..18c2ebe2f317 --- /dev/null +++ b/worlds/osrs/LogicCSV/resources_generated.py @@ -0,0 +1,54 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Regions import ResourceRow + +resource_rows = [ + ResourceRow('Mind Runes'), + ResourceRow('Spinning Wheel'), + ResourceRow('Sheep'), + ResourceRow('Furnace'), + ResourceRow('Chisel'), + ResourceRow('Bronze Ores'), + ResourceRow('Iron Ore'), + ResourceRow('Silver Ore'), + ResourceRow('Coal Ore'), + ResourceRow('Gold Ore'), + ResourceRow('Bronze Anvil'), + ResourceRow('Anvil'), + ResourceRow('Shrimp Spot'), + ResourceRow('Fly Fishing Spot'), + ResourceRow('Lobster Spot'), + ResourceRow('Redberry Bush'), + ResourceRow('Bowl'), + ResourceRow('Meat'), + ResourceRow('Cooking Apple'), + ResourceRow('Pie Dish'), + ResourceRow('Cake Tin'), + ResourceRow('Wheat'), + ResourceRow('Windmill'), + ResourceRow('Egg'), + ResourceRow('Milk'), + ResourceRow('Cheese'), + ResourceRow('Tomato'), + ResourceRow('Oak Tree'), + ResourceRow('Willow Tree'), + ResourceRow('Canoe Tree'), + ResourceRow('Goblin'), + ResourceRow('Barbarian'), + ResourceRow('Zombie'), + ResourceRow('Guard'), + ResourceRow('Hill Giant'), + ResourceRow('Deadly Red Spider'), + ResourceRow('Moss Giant'), + ResourceRow('Ice Giant'), + ResourceRow('Lesser Demon'), + ResourceRow('Rune Essence'), + ResourceRow('Crafting Moulds'), + ResourceRow('Nature Runes'), + ResourceRow('Law Runes'), + ResourceRow('Imps'), + ResourceRow('Clay Ore'), + ResourceRow('Onion'), + ResourceRow('Potato'), +] diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py new file mode 100644 index 000000000000..1a44aa389c6a --- /dev/null +++ b/worlds/osrs/Names.py @@ -0,0 +1,212 @@ +from enum import Enum + + +class RegionNames(str, Enum): + Lumbridge = "Lumbridge" + Lumbridge_Swamp = "Lumbridge Swamp" + Lumbridge_Farms_East = "Lumbridge Farms East" + Lumbridge_Farms_West = "Lumbridge Farms West" + HAM_Hideout = "HAM Hideout" + Draynor_Village = "Draynor Village" + Draynor_Manor = "Draynor Manor" + Wizards_Tower = "Wizard Tower" + Al_Kharid = "Al Kharid" + Citharede_Abbey = "Citharede Abbey" + South_Of_Varrock = "South of Varrock" + Central_Varrock = "Central Varrock" + Varrock_Palace = "Varrock Palace" + East_Of_Varrock = "East Varrock" + West_Varrock = "West Varrock" + Edgeville = "Edgeville" + Barbarian_Village = "Barbarian Village" + Monastery = "Monastery" + Ice_Mountain = "Ice Mountain" + Dwarven_Mines = "Dwarven Mines" + Falador = "Falador" + Falador_Farm = "Falador Farms" + Crafting_Guild = "Crafting Guild" + Cooks_Guild = "Cook's Guild" + Rimmington = "Rimmington" + Port_Sarim = "Port Sarim" + Mudskipper_Point = "Mudskipper Point" + Karamja = "Karamja" + Corsair_Cove = "Corsair Cove" + Wilderness = "Wilderness" + Crandor = "Crandor" + # Resource Regions + Egg = "Egg" + Sheep = "Sheep" + Milk = "Milk" + Wheat = "Wheat" + Windmill = "Windmill" + Spinning_Wheel = "Spinning Wheel" + Imp = "Imp" + Bronze_Ores = "Bronze Ores" + Clay_Rock = "Clay Ore" + Coal_Rock = "Coal Ore" + Iron_Rock = "Iron Ore" + Silver_Rock = "Silver Ore" + Gold_Rock = "Gold Ore" + Furnace = "Furnace" + Anvil = "Anvil" + Oak_Tree = "Oak Tree" + Willow_Tree = "Willow Tree" + Shrimp = "Shrimp Spot" + Fly_Fish = "Fly Fishing Spot" + Lobster = "Lobster Spot" + Mind_Runes = "Mind Runes" + Canoe_Tree = "Canoe Tree" + + __str__ = str.__str__ + + +class ItemNames(str, Enum): + Lumbridge = "Area: Lumbridge" + Lumbridge_Swamp = "Area: Lumbridge Swamp" + Lumbridge_Farms = "Area: Lumbridge Farms" + HAM_Hideout = "Area: HAM Hideout" + Draynor_Village = "Area: Draynor Village" + Draynor_Manor = "Area: Draynor Manor" + Wizards_Tower = "Area: Wizard Tower" + Al_Kharid = "Area: Al Kharid" + Citharede_Abbey = "Area: Citharede Abbey" + South_Of_Varrock = "Area: South of Varrock" + Central_Varrock = "Area: Central Varrock" + Varrock_Palace = "Area: Varrock Palace" + East_Of_Varrock = "Area: East Varrock" + West_Varrock = "Area: West Varrock" + Edgeville = "Area: Edgeville" + Barbarian_Village = "Area: Barbarian Village" + Monastery = "Area: Monastery" + Ice_Mountain = "Area: Ice Mountain" + Dwarven_Mines = "Area: Dwarven Mines" + Falador = "Area: Falador" + Falador_Farm = "Area: Falador Farms" + Crafting_Guild = "Area: Crafting Guild" + Rimmington = "Area: Rimmington" + Port_Sarim = "Area: Port Sarim" + Mudskipper_Point = "Area: Mudskipper Point" + Karamja = "Area: Karamja" + Crandor = "Area: Crandor" + Corsair_Cove = "Area: Corsair Cove" + Wilderness = "Area: Wilderness" + Progressive_Armor = "Progressive Armor" + Progressive_Weapons = "Progressive Weapons" + Progressive_Tools = "Progressive Tools" + Progressive_Range_Armor = "Progressive Ranged Armor" + Progressive_Range_Weapon = "Progressive Ranged Weapons" + Progressive_Magic = "Progressive Magic" + Lobsters = "10 Lobsters" + Swordfish = "5 Swordfish" + Energy_Potions = "10 Energy Potions" + Coins = "5,000 Coins" + Mind_Runes = "50 Mind Runes" + Chaos_Runes = "25 Chaos Runes" + Death_Runes = "10 Death Runes" + Law_Runes = "10 Law Runes" + QP_Cooks_Assistant = "1 QP (Cook's Assistant)" + QP_Demon_Slayer = "3 QP (Demon Slayer)" + QP_Restless_Ghost = "1 QP (The Restless Ghost)" + QP_Romeo_Juliet = "5 QP (Romeo & Juliet)" + QP_Sheep_Shearer = "1 QP (Sheep Shearer)" + QP_Shield_of_Arrav = "1 QP (Shield of Arrav)" + QP_Ernest_the_Chicken = "4 QP (Ernest the Chicken)" + QP_Vampyre_Slayer = "3 QP (Vampyre Slayer)" + QP_Imp_Catcher = "1 QP (Imp Catcher)" + QP_Prince_Ali_Rescue = "3 QP (Prince Ali Rescue)" + QP_Dorics_Quest = "1 QP (Doric's Quest)" + QP_Black_Knights_Fortress = "3 QP (Black Knights' Fortress)" + QP_Witchs_Potion = "1 QP (Witch's Potion)" + QP_Knights_Sword = "1 QP (The Knight's Sword)" + QP_Goblin_Diplomacy = "5 QP (Goblin Diplomacy)" + QP_Pirates_Treasure = "2 QP (Pirate's Treasure)" + QP_Rune_Mysteries = "1 QP (Rune Mysteries)" + QP_Misthalin_Mystery = "1 QP (Misthalin Mystery)" + QP_Corsair_Curse = "2 QP (The Corsair Curse)" + QP_X_Marks_the_Spot = "1 QP (X Marks The Spot)" + QP_Below_Ice_Mountain = "1 QP (Below Ice Mountain)" + + __str__ = str.__str__ + + +class LocationNames(str, Enum): + Q_Cooks_Assistant = "Quest: Cook's Assistant" + Q_Demon_Slayer = "Quest: Demon Slayer" + Q_Restless_Ghost = "Quest: The Restless Ghost" + Q_Romeo_Juliet = "Quest: Romeo & Juliet" + Q_Sheep_Shearer = "Quest: Sheep Shearer" + Q_Shield_of_Arrav = "Quest: Shield of Arrav" + Q_Ernest_the_Chicken = "Quest: Ernest the Chicken" + Q_Vampyre_Slayer = "Quest: Vampyre Slayer" + Q_Imp_Catcher = "Quest: Imp Catcher" + Q_Prince_Ali_Rescue = "Quest: Prince Ali Rescue" + Q_Dorics_Quest = "Quest: Doric's Quest" + Q_Black_Knights_Fortress = "Quest: Black Knights' Fortress" + Q_Witchs_Potion = "Quest: Witch's Potion" + Q_Knights_Sword = "Quest: The Knight's Sword" + Q_Goblin_Diplomacy = "Quest: Goblin Diplomacy" + Q_Pirates_Treasure = "Quest: Pirate's Treasure" + Q_Rune_Mysteries = "Quest: Rune Mysteries" + Q_Misthalin_Mystery = "Quest: Misthalin Mystery" + Q_Corsair_Curse = "Quest: The Corsair Curse" + Q_X_Marks_the_Spot = "Quest: X Marks the Spot" + Q_Below_Ice_Mountain = "Quest: Below Ice Mountain" + QP_Cooks_Assistant = "Points: Cook's Assistant" + QP_Demon_Slayer = "Points: Demon Slayer" + QP_Restless_Ghost = "Points: The Restless Ghost" + QP_Romeo_Juliet = "Points: Romeo & Juliet" + QP_Sheep_Shearer = "Points: Sheep Shearer" + QP_Shield_of_Arrav = "Points: Shield of Arrav" + QP_Ernest_the_Chicken = "Points: Ernest the Chicken" + QP_Vampyre_Slayer = "Points: Vampyre Slayer" + QP_Imp_Catcher = "Points: Imp Catcher" + QP_Prince_Ali_Rescue = "Points: Prince Ali Rescue" + QP_Dorics_Quest = "Points: Doric's Quest" + QP_Black_Knights_Fortress = "Points: Black Knights' Fortress" + QP_Witchs_Potion = "Points: Witch's Potion" + QP_Knights_Sword = "Points: The Knight's Sword" + QP_Goblin_Diplomacy = "Points: Goblin Diplomacy" + QP_Pirates_Treasure = "Points: Pirate's Treasure" + QP_Rune_Mysteries = "Points: Rune Mysteries" + QP_Misthalin_Mystery = "Points: Misthalin Mystery" + QP_Corsair_Curse = "Points: The Corsair Curse" + QP_X_Marks_the_Spot = "Points: X Marks the Spot" + QP_Below_Ice_Mountain = "Points: Below Ice Mountain" + Guppy = "Prepare a Guppy" + Cavefish = "Prepare a Cavefish" + Tetra = "Prepare a Tetra" + Barronite_Deposit = "Crush a Barronite Deposit" + Oak_Log = "Cut an Oak Log" + Willow_Log = "Cut a Willow Log" + Catch_Lobster = "Catch a Lobster" + Mine_Silver = "Mine Silver" + Mine_Coal = "Mine Coal" + Mine_Gold = "Mine Gold" + Smelt_Silver = "Smelt a Silver Bar" + Smelt_Steel = "Smelt a Steel Bar" + Smelt_Gold = "Smelt a Gold Bar" + Cut_Sapphire = "Cut a Sapphire" + Cut_Emerald = "Cut an Emerald" + Cut_Ruby = "Cut a Ruby" + Cut_Diamond = "Cut a Diamond" + K_Lesser_Demon = "Kill a Lesser Demon" + K_Ogress_Shaman = "Kill an Ogress Shaman" + Bake_Apple_Pie = "Bake an Apple Pie" + Bake_Cake = "Bake a Cake" + Bake_Meat_Pizza = "Bake a Meat Pizza" + Total_XP_5000 = "5,000 Total XP" + Total_XP_10000 = "10,000 Total XP" + Total_XP_25000 = "25,000 Total XP" + Total_XP_50000 = "50,000 Total XP" + Total_XP_100000 = "100,000 Total XP" + Total_Level_50 = "Total Level 50" + Total_Level_100 = "Total Level 100" + Total_Level_150 = "Total Level 150" + Total_Level_200 = "Total Level 200" + Combat_Level_5 = "Combat Level 5" + Combat_Level_15 = "Combat Level 15" + Combat_Level_25 = "Combat Level 25" + Travel_on_a_Canoe = "Travel on a Canoe" + Q_Dragon_Slayer = "Quest: Dragon Slayer" + + __str__ = str.__str__ diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py new file mode 100644 index 000000000000..81e017eddb34 --- /dev/null +++ b/worlds/osrs/Options.py @@ -0,0 +1,509 @@ +from dataclasses import dataclass + +from Options import Choice, Toggle, Range, PerGameCommonOptions + +MAX_COMBAT_TASKS = 16 +MAX_PRAYER_TASKS = 3 +MAX_MAGIC_TASKS = 4 +MAX_RUNECRAFT_TASKS = 3 +MAX_CRAFTING_TASKS = 5 +MAX_MINING_TASKS = 5 +MAX_SMITHING_TASKS = 4 +MAX_FISHING_TASKS = 5 +MAX_COOKING_TASKS = 5 +MAX_FIREMAKING_TASKS = 2 +MAX_WOODCUTTING_TASKS = 3 + +NON_QUEST_LOCATION_COUNT = 22 + + +class StartingArea(Choice): + """ + Which chunks are available at the start. The player may need to move through locked chunks to reach the starting + area, but any areas that require quests, skills, or coins are not available as a starting location. + + "Any Bank" rolls a random region that contains a bank. + Chunksanity can start you in any chunk. Hope you like woodcutting! + """ + display_name = "Starting Region" + option_lumbridge = 0 + option_al_kharid = 1 + option_varrock_east = 2 + option_varrock_west = 3 + option_edgeville = 4 + option_falador = 5 + option_draynor = 6 + option_wilderness = 7 + option_any_bank = 8 + option_chunksanity = 9 + default = 0 + + +class BrutalGrinds(Toggle): + """ + Whether to allow skill tasks without having reasonable access to the usual skill training path. + For example, if enabled, you could be forced to train smithing without an anvil purely by smelting bars, + or training fishing to high levels entirely on shrimp. + """ + display_name = "Allow Brutal Grinds" + + +class ProgressiveTasks(Toggle): + """ + Whether skill tasks should always be generated in order of easiest to hardest. + If enabled, you would not be assigned "Mine Gold" without also being assigned + "Mine Silver", "Mine Coal", and "Mine Iron". Enabling this will result in a generally shorter seed, but with + a lower variety of tasks. + """ + display_name = "Progressive Tasks" + + +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 + + +class MaxCombatTasks(Range): + """ + The maximum number of Combat Tasks to possibly be assigned. + 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 + + +class CombatTaskWeight(Range): + """ + How much to favor generating combat tasks over other types of task. + 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 + + +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 + + +class MaxPrayerTasks(Range): + """ + The maximum number of Prayer Tasks to possibly be assigned. + 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 + + +class PrayerTaskWeight(Range): + """ + How much to favor generating Prayer tasks over other types of task. + 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 + + +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 + + +class MaxMagicTasks(Range): + """ + The maximum number of Magic Tasks to possibly be assigned. + 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 + + +class MagicTaskWeight(Range): + """ + How much to favor generating Magic tasks over other types of task. + 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 + + +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 + + +class MaxRunecraftTasks(Range): + """ + The maximum number of Runecraft Tasks to possibly be assigned. + 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 + + +class RunecraftTaskWeight(Range): + """ + How much to favor generating Runecraft tasks over other types of task. + 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 + + +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 + + +class MaxCraftingTasks(Range): + """ + The maximum number of Crafting Tasks to possibly be assigned. + 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 + + +class CraftingTaskWeight(Range): + """ + How much to favor generating Crafting tasks over other types of task. + 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 + + +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 + + +class MaxMiningTasks(Range): + """ + The maximum number of Mining Tasks to possibly be assigned. + 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 + + +class MiningTaskWeight(Range): + """ + How much to favor generating Mining tasks over other types of task. + 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 + + +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 + + +class MaxSmithingTasks(Range): + """ + The maximum number of Smithing Tasks to possibly be assigned. + 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 + + +class SmithingTaskWeight(Range): + """ + How much to favor generating Smithing tasks over other types of task. + 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 + + +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 + + +class MaxFishingTasks(Range): + """ + The maximum number of Fishing Tasks to possibly be assigned. + 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 + + +class FishingTaskWeight(Range): + """ + How much to favor generating Fishing tasks over other types of task. + 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 + + +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 + + +class MaxCookingTasks(Range): + """ + The maximum number of Cooking Tasks to possibly be assigned. + 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 + + +class CookingTaskWeight(Range): + """ + How much to favor generating Cooking tasks over other types of task. + 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 + + +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 + + +class MaxFiremakingTasks(Range): + """ + The maximum number of Firemaking Tasks to possibly be assigned. + 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 + + +class FiremakingTaskWeight(Range): + """ + How much to favor generating Firemaking tasks over other types of task. + 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 + + +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 + + +class MaxWoodcuttingTasks(Range): + """ + The maximum number of Woodcutting Tasks to possibly be assigned. + 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 + + +class WoodcuttingTaskWeight(Range): + """ + How much to favor generating Woodcutting tasks over other types of task. + 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 + + +class MinimumGeneralTasks(Range): + """ + How many guaranteed general progression tasks to be assigned (total level, total XP, etc.). + 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 + + +class GeneralTaskWeight(Range): + """ + How much to favor generating General tasks over other types of task. + 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 + + +@dataclass +class OSRSOptions(PerGameCommonOptions): + starting_area: StartingArea + brutal_grinds: BrutalGrinds + progressive_tasks: ProgressiveTasks + max_combat_level: MaxCombatLevel + max_combat_tasks: MaxCombatTasks + combat_task_weight: CombatTaskWeight + max_prayer_level: MaxPrayerLevel + max_prayer_tasks: MaxPrayerTasks + prayer_task_weight: PrayerTaskWeight + max_magic_level: MaxMagicLevel + max_magic_tasks: MaxMagicTasks + magic_task_weight: MagicTaskWeight + max_runecraft_level: MaxRunecraftLevel + max_runecraft_tasks: MaxRunecraftTasks + runecraft_task_weight: RunecraftTaskWeight + max_crafting_level: MaxCraftingLevel + max_crafting_tasks: MaxCraftingTasks + crafting_task_weight: CraftingTaskWeight + max_mining_level: MaxMiningLevel + max_mining_tasks: MaxMiningTasks + mining_task_weight: MiningTaskWeight + max_smithing_level: MaxSmithingLevel + max_smithing_tasks: MaxSmithingTasks + smithing_task_weight: SmithingTaskWeight + max_fishing_level: MaxFishingLevel + max_fishing_tasks: MaxFishingTasks + fishing_task_weight: FishingTaskWeight + max_cooking_level: MaxCookingLevel + max_cooking_tasks: MaxCookingTasks + cooking_task_weight: CookingTaskWeight + max_firemaking_level: MaxFiremakingLevel + max_firemaking_tasks: MaxFiremakingTasks + firemaking_task_weight: FiremakingTaskWeight + max_woodcutting_level: MaxWoodcuttingLevel + max_woodcutting_tasks: MaxWoodcuttingTasks + woodcutting_task_weight: WoodcuttingTaskWeight + minimum_general_tasks: MinimumGeneralTasks + general_task_weight: GeneralTaskWeight diff --git a/worlds/osrs/Regions.py b/worlds/osrs/Regions.py new file mode 100644 index 000000000000..436cdf3c7c78 --- /dev/null +++ b/worlds/osrs/Regions.py @@ -0,0 +1,12 @@ +import typing + + +class RegionRow(typing.NamedTuple): + name: str + itemReq: str + connections: typing.List[str] + resources: typing.List[str] + + +class ResourceRow(typing.NamedTuple): + name: str diff --git a/worlds/osrs/Rules.py b/worlds/osrs/Rules.py new file mode 100644 index 000000000000..22a19934c8e1 --- /dev/null +++ b/worlds/osrs/Rules.py @@ -0,0 +1,337 @@ +""" + Ensures a target level can be reached with available resources + """ +from worlds.generic.Rules import CollectionRule, add_rule +from .Names import RegionNames, ItemNames + + +def get_fishing_skill_rule(level, player, options) -> CollectionRule: + if options.max_fishing_level < level: + return lambda state: False + + if options.brutal_grinds or level < 5: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) + if level < 20: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) + else: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) and \ + state.can_reach_region(RegionNames.Fly_Fish, player) + + +def get_mining_skill_rule(level, player, options) -> CollectionRule: + if options.max_mining_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \ + state.can_reach_region(RegionNames.Clay_Rock, player) + else: + # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for + return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or + state.can_reach_region(RegionNames.Clay_Rock, player)) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) + + +def get_woodcutting_skill_rule(level, player, options) -> CollectionRule: + if options.max_woodcutting_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. + # Even the desert. + return lambda state: True + if level < 30: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) + else: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \ + state.can_reach_region(RegionNames.Willow_Tree, player) + + +def get_smithing_skill_rule(level, player, options) -> CollectionRule: + if options.max_smithing_level < level: + return lambda state: False + + if options.brutal_grinds: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) + if level < 15: + # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included + # in the "Anvil" resource region. We still need to check for it though. + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + (state.can_reach_region(RegionNames.Anvil, player) or + state.can_reach_region(RegionNames.Lumbridge, player)) + if level < 30: + # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + else: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Coal_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + + +def get_crafting_skill_rule(level, player, options): + if options.max_crafting_level < level: + return lambda state: False + + # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable + def can_spin(state): + return state.can_reach_region(RegionNames.Sheep, player) and \ + state.can_reach_region(RegionNames.Spinning_Wheel, player) + + def can_pot(state): + return state.can_reach_region(RegionNames.Clay_Rock, player) and \ + state.can_reach_region(RegionNames.Barbarian_Village, player) + + def can_tan(state): + return state.can_reach_region(RegionNames.Milk, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + def mould_access(state): + return state.can_reach_region(RegionNames.Al_Kharid, player) or \ + state.can_reach_region(RegionNames.Rimmington, player) + + def can_silver(state): + return state.can_reach_region(RegionNames.Silver_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + def can_gold(state): + return state.can_reach_region(RegionNames.Gold_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + if options.brutal_grinds or level < 5: + return lambda state: can_spin(state) or can_pot(state) or can_tan(state) + + can_smelt_gold = get_smithing_skill_rule(40, player, options) + can_smelt_silver = get_smithing_skill_rule(20, player, options) + if level < 16: + return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_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)) + + +def get_cooking_skill_rule(level, player, options) -> CollectionRule: + if options.max_cooking_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Milk, player) or \ + state.can_reach_region(RegionNames.Egg, player) or \ + state.can_reach_region(RegionNames.Shrimp, player) or \ + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + else: + can_catch_fly_fish = get_fishing_skill_rule(20, player, options) + + return lambda state: ( + (state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or + (state.can_reach_region(RegionNames.Port_Sarim, player)) + ) and ( + state.can_reach_region(RegionNames.Milk, player) or + state.can_reach_region(RegionNames.Egg, player) or + state.can_reach_region(RegionNames.Shrimp, player) or + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + ) + + +def get_runecraft_skill_rule(level, player, options) -> CollectionRule: + if options.max_runecraft_level < level: + return lambda state: False + if not options.brutal_grinds: + # Ensure access to the relevant altars + if level >= 5: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) + if level >= 9: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) + if level >= 14: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) + + +def get_magic_skill_rule(level, player, options) -> CollectionRule: + if options.max_magic_level < level: + return lambda state: False + + return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player) + + +def get_firemaking_skill_rule(level, player, options) -> CollectionRule: + if options.max_firemaking_level < level: + return lambda state: False + if not options.brutal_grinds: + if level >= 30: + can_chop_willows = get_woodcutting_skill_rule(30, player, options) + return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state) + if level >= 15: + can_chop_oaks = get_woodcutting_skill_rule(15, player, options) + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state) + # If brutal grinds are on, or if the level is less than 15, you can train it. + return lambda state: True + + +def get_skill_rule(skill, level, player, options) -> CollectionRule: + if skill.lower() == "fishing": + return get_fishing_skill_rule(level, player, options) + if skill.lower() == "mining": + return get_mining_skill_rule(level, player, options) + if skill.lower() == "woodcutting": + return get_woodcutting_skill_rule(level, player, options) + if skill.lower() == "smithing": + return get_smithing_skill_rule(level, player, options) + if skill.lower() == "crafting": + return get_crafting_skill_rule(level, player, options) + if skill.lower() == "cooking": + return get_cooking_skill_rule(level, player, options) + if skill.lower() == "runecraft": + return get_runecraft_skill_rule(level, player, options) + if skill.lower() == "magic": + return get_magic_skill_rule(level, player, options) + if skill.lower() == "firemaking": + return get_firemaking_skill_rule(level, player, options) + + return lambda state: True + + +def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options): + if outbound_region_name == RegionNames.Cooks_Guild: + add_rule(entrance, get_cooking_skill_rule(32, player, options)) + elif outbound_region_name == RegionNames.Crafting_Guild: + add_rule(entrance, get_crafting_skill_rule(40, player, options)) + elif outbound_region_name == RegionNames.Corsair_Cove: + # Need to be able to start Corsair Curse in addition to having the item + add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player)) + elif outbound_region_name == "Camdozaal*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player)) + elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player)) + + # Special logic for canoes + canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, + RegionNames.Edgeville, RegionNames.Wilderness] + if region_row.name in canoe_regions: + # Skill rules for greater distances + woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options) + woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options) + woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options) + woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options) + + if region_row.name == RegionNames.Lumbridge: + # Canoe Tree access for the Location + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_all(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + + elif region_row.name == RegionNames.South_Of_Varrock: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d3(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + elif region_row.name == RegionNames.Barbarian_Village: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d2(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + # Edgeville does not need to be checked, because it's already adjacent + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_d3) + elif region_row.name == RegionNames.Edgeville: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d2) + # Barbarian Village does not need to be checked, because it's already adjacent + # Wilderness does not need to be checked, because it's already adjacent + elif region_row.name == RegionNames.Wilderness: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_all(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_all) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + # Edgeville does not need to be checked, because it's already adjacent diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py new file mode 100644 index 000000000000..d6ddd63875f4 --- /dev/null +++ b/worlds/osrs/__init__.py @@ -0,0 +1,404 @@ +import typing + +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState +from Fill import fill_restrictive, FillError +from worlds.AutoWorld import WebWorld, World +from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ + chunksanity_special_region_names +from .Locations import OSRSLocation, LocationRow +from .Rules import * +from .Options import OSRSOptions, StartingArea +from .Names import LocationNames, ItemNames, RegionNames + +from .LogicCSV.LogicCSVToPython import data_csv_tag +from .LogicCSV.items_generated import item_rows +from .LogicCSV.locations_generated import location_rows +from .LogicCSV.regions_generated import region_rows +from .LogicCSV.resources_generated import resource_rows +from .Regions import RegionRow, ResourceRow + + +class OSRSWeb(WebWorld): + theme = "stone" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Old School Runescape Randomizer connected to an Archipelago Multiworld", + "English", + "docs/setup_en.md", + "setup/en", + ["digiholic"] + ) + tutorials = [setup_en] + + +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 + topology_present = True + web = OSRSWeb() + base_id = 0x070000 + data_version = 1 + explicit_indirect_conditions = False + + item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} + location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} + + region_name_to_data: typing.Dict[str, Region] + location_name_to_data: typing.Dict[str, OSRSLocation] + + location_rows_by_name: typing.Dict[str, LocationRow] + region_rows_by_name: typing.Dict[str, RegionRow] + resource_rows_by_name: typing.Dict[str, ResourceRow] + item_rows_by_name: typing.Dict[str, ItemRow] + + starting_area_item: str + + locations_by_category: typing.Dict[str, typing.List[LocationRow]] + available_QP_locations: typing.List[str] + + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) + self.region_name_to_data = {} + self.location_name_to_data = {} + + self.location_rows_by_name = {} + self.region_rows_by_name = {} + self.resource_rows_by_name = {} + self.item_rows_by_name = {} + + self.starting_area_item = "" + + self.locations_by_category = {} + self.available_QP_locations = [] + + def generate_early(self) -> None: + location_categories = [location_row.category for location_row in location_rows] + self.locations_by_category = {category: + [location_row for location_row in location_rows if + location_row.category == category] + for category in location_categories} + + self.location_rows_by_name = {loc_row.name: loc_row for loc_row in location_rows} + self.region_rows_by_name = {reg_row.name: reg_row for reg_row in region_rows} + self.resource_rows_by_name = {rec_row.name: rec_row for rec_row in resource_rows} + self.item_rows_by_name = {it_row.name: it_row for it_row in item_rows} + + 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) + + # 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. + _Make sure to update that value whenever the CSVs change!_ + """ + + 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 + during generate_early or basic as well. + """ + + # First, create the "Menu" region to start + menu_region = self.create_region("Menu") + + for region_row in region_rows: + self.create_region(region_row.name) + + for resource_row in resource_rows: + self.create_region(resource_row.name) + + # 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 area hasn't been set, then we shouldn't connect it + if self.starting_area_item != "": + 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: + region = self.region_name_to_data[region_row.name] + + for outbound_region_name in region_row.connections: + parsed_outbound = outbound_region_name.replace('*', '') + entrance = region.create_exit(f"{region_row.name}->{parsed_outbound}") + entrance.connect(self.region_name_to_data[parsed_outbound]) + + item_name = self.region_rows_by_name[parsed_outbound].itemReq + entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player) + generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options) + + for resource_region in region_row.resources: + if not resource_region: + continue + + entrance = region.create_exit(f"{region_row.name}->{resource_region.replace('*', '')}") + if "*" not in resource_region: + entrance.connect(self.region_name_to_data[resource_region]) + else: + entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) + generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options) + + self.roll_locations() + + def task_within_skill_levels(self, skills_required): + # Loop through each required skill. If any of its requirements are out of the defined limit, return false + for skill in skills_required: + max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level") + if skill.level > max_level_for_skill: + return False + return True + + def roll_locations(self): + generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override + locations_required = 0 + for item_row in item_rows: + locations_required += item_row.amount + + locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 + + # Quests are always added first, before anything else is rolled + for i, location_row in enumerate(location_rows): + if location_row.category in {"quest", "points", "goal"}: + if self.task_within_skill_levels(location_row.skills): + self.create_and_add_location(i) + if location_row.category == "quest": + locations_added += 1 + + # Build up the weighted Task Pool + rnd = self.random + + # Start with the minimum general tasks + general_tasks = [task for task in self.locations_by_category["general"]] + if not self.options.progressive_tasks: + rnd.shuffle(general_tasks) + else: + general_tasks.reverse() + for i in range(self.options.minimum_general_tasks): + task = general_tasks.pop() + self.add_location(task) + locations_added += 1 + + general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0 + + tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {} + weights_per_task_type: typing.Dict[str, int] = {} + + task_types = ["prayer", "magic", "runecraft", "mining", "crafting", + "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] + for task_type in task_types: + max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") + tasks_for_this_type = [task for task in self.locations_by_category[task_type] + if self.task_within_skill_levels(task.skills)] + if not self.options.progressive_tasks: + rnd.shuffle(tasks_for_this_type) + else: + tasks_for_this_type.reverse() + + tasks_for_this_type = tasks_for_this_type[:max_amount_for_task_type] + weight_for_this_type = getattr(self.options, + f"{task_type}_task_weight") + if weight_for_this_type > 0 and tasks_for_this_type: + tasks_per_task_type[task_type] = tasks_for_this_type + weights_per_task_type[task_type] = weight_for_this_type + + # Build a list of collections and weights in a matching order for rnd.choices later + all_tasks = [] + all_weights = [] + for task_type in task_types: + if task_type in tasks_per_task_type: + all_tasks.append(tasks_per_task_type[task_type]) + all_weights.append(weights_per_task_type[task_type]) + + # Even after the initial forced generals, they can still be rolled randomly + if general_weight > 0: + all_tasks.append(general_tasks) + all_weights.append(general_weight) + + while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0): + if all_tasks: + chosen_task = rnd.choices(all_tasks, all_weights)[0] + if chosen_task: + task = chosen_task.pop() + self.add_location(task) + locations_added += 1 + + # This isn't an else because chosen_task can become empty in the process of resolving the above block + # We still want to clear this list out while we're doing that + if not chosen_task: + index = all_tasks.index(chosen_task) + del all_tasks[index] + del all_weights[index] + + else: + if len(general_tasks) == 0: + raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " + + f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.") + task = general_tasks.pop() + self.add_location(task) + locations_added += 1 + + + def add_location(self, location): + index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] + self.create_and_add_location(index) + + def create_items(self) -> None: + for item_row in item_rows: + if item_row.name != self.starting_area_item: + for c in range(item_row.amount): + item = self.create_item(item_row.name) + self.multiworld.itempool.append(item) + + def get_filler_item_name(self) -> str: + return self.random.choice( + [ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic, + ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon]) + + def create_and_add_location(self, row_index) -> None: + location_row = location_rows[row_index] + + # Quest Points are handled differently now, but in case this gets fed an older version of the data sheet, + # the points might still be listed in a different row + if location_row.category == "points": + return + + # Create Location + location_id = self.base_id + row_index + if location_row.category == "goal": + location_id = None + location = OSRSLocation(self.player, location_row.name, location_id) + self.location_name_to_data[location_row.name] = location + + # Add the location to its first region, or if it doesn't belong to one, to Menu + region = self.region_name_to_data["Menu"] + if location_row.regions: + region = self.region_name_to_data[location_row.regions[0]] + location.parent_region = region + region.locations.append(location) + + # If it's a quest, generate a "Points" location we'll add an event to + if location_row.category == "quest": + points_name = location_row.name.replace("Quest:", "Points:") + points_location = OSRSLocation(self.player, points_name) + self.location_name_to_data[points_name] = points_location + points_location.parent_region = region + region.locations.append(points_location) + + def set_rules(self) -> None: + """ + called to set access and item rules on locations and entrances. + """ + quest_attr_names = ["Cooks_Assistant", "Demon_Slayer", "Restless_Ghost", "Romeo_Juliet", + "Sheep_Shearer", "Shield_of_Arrav", "Ernest_the_Chicken", "Vampyre_Slayer", + "Imp_Catcher", "Prince_Ali_Rescue", "Dorics_Quest", "Black_Knights_Fortress", + "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", + "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", + "Below_Ice_Mountain"] + + for quest_attr_name in quest_attr_names: + qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") + qp_loc = self.location_name_to_data.get(qp_loc_name) + + q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") + q_loc = self.location_name_to_data.get(q_loc_name) + + # Checks to make sure the task is actually in the list before trying to create its rules + if qp_loc and q_loc: + # Create the QP Event Item + item_name = getattr(ItemNames, f"QP_{quest_attr_name}") + qp_loc.place_locked_item(self.create_event(item_name)) + + # If a quest is excluded, don't actually consider it for quest point progression + if q_loc_name not in self.options.exclude_locations: + self.available_QP_locations.append(item_name) + + # Set the access rule for the QP Location + add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state))) + + # place "Victory" at "Dragon Slayer" and set collection as win condition + self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ + .place_locked_item(self.create_event("Victory")) + self.multiworld.completion_condition[self.player] = lambda state: (state.has("Victory", self.player)) + + for location_name, location in self.location_name_to_data.items(): + location_row = self.location_rows_by_name[location_name] + # Set up requirements for region + for region_required_name in location_row.regions: + region_required = self.region_name_to_data[region_required_name] + add_rule(location, + lambda state, region_required=region_required: state.can_reach(region_required, "Region", + self.player)) + for skill_req in location_row.skills: + add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options)) + for item_req in location_row.items: + add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) + if location_row.qp: + add_rule(location, lambda state, location_row=location_row: self.quest_points(state) > location_row.qp) + + def create_region(self, name: str) -> "Region": + region = Region(name, self.player, self.multiworld) + self.region_name_to_data[name] = region + self.multiworld.regions.append(region) + return region + + def create_item(self, item_name: str) -> "Item": + items = [item for item in item_rows if item.name == item_name] + assert len(items) > 0, f"No matching item found for name {item_name} for player {self.player_name}" + item = items[0] + index = item_rows.index(item) + return OSRSItem(item.name, item.progression, self.base_id + index, self.player) + + def create_event(self, event: str): + # while we are at it, we can also add a helper to create events + return OSRSItem(event, ItemClassification.progression, None, self.player) + + def quest_points(self, state): + qp = 0 + for qp_event in self.available_QP_locations: + if state.has(qp_event, self.player): + qp += int(qp_event[0]) + return qp + diff --git a/worlds/osrs/docs/en_Old School Runescape.md b/worlds/osrs/docs/en_Old School Runescape.md new file mode 100644 index 000000000000..d367082b2274 --- /dev/null +++ b/worlds/osrs/docs/en_Old School Runescape.md @@ -0,0 +1,114 @@ +# Old School Runescape + +## What is the Goal of this Randomizer? +The goal is to complete the quest "Dragon Slayer I" with limited access to gear and map chunks while following normal +Ironman/Group Ironman restrictions on a fresh free-to-play account. + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. OSRS contains many options for a highly customizable experience. The options available to you are: + +* **Starting Area** - The starting region of your run. This is the first region you will have available, and you can always +freely return to it (see the section below for when it is allowed to cross locked regions to access it) + * You may select a starting city from the list of Lumbridge, Al Kharid, Varrock (East or West), Edgeville, Falador, +Draynor Village, or The Wilderness (Ferox Enclave) + * The option "Any Bank" will choose one of the above regions at random + * The option "Chunksanity" can start you in _any_ chunk, regardless of whether it has access to a bank. +* **Brutal Grinds** - If enabled, the logic will assume you are willing to go to great lengths to train skills. + * As an example, when enabled, it might be in logic to obtain tin and copper from mob drops and smelt bronze bars to +reach Smithing Level 40 to smelt gold for a task. + * If left disabled, the logic will always ensure you have a reasonable method for training a skill to reach a specific +task, such as having access to intermediate-level training options +* **Progressive Tasks** - If enabled, tasks for a skill are generated in order from earliest to latest. + * For example, your first Smithing task would always be "Smelt an Iron Bar", then "Smelt a Silver Bar", and so on. +You would never have the task "Smelt a Gold Bar" without having every previous Smithing task as well. +This can lead to a more consistent length of run, and is generally shorter than disabling it, but with less variety. +* **Skill Category Weighting Options** + * These are available in each task category (all trainable skills plus "Combat" and "General") + * **Max [Category] Level** - The highest level you intend to have to reach in order to complete all tasks for this +category. For the Combat category, this is the max level of monster you are willing to fight. +General tasks do not have a level and thus do not have this option. + * **Max [Category] Tasks** - The highest number of tasks in this category you are willing to be assigned. +Note that you can end up with _less_ than this amount, but never more. The "General" category is used to fill remaining +spots so a maximum is not specified, instead it has a _minimum_ count. + * **[Category] Task Weighting** - The relative weighting of this category to all of the others. Increase this to make +tasks in this category more likely. + +## What does randomization do to this game? +The OSRS Archipelago Randomizer takes the form of a "Chunkman" account, a form of challenge account +where you are limited to specific regions of the map (known as "chunks") until you complete tasks to unlock +more. The plugin will interface with the [Region Locker Plugin](https://github.com/slaytostay/region-locker) to +visually display these chunk borders and highlight them as locked or unlocked. The optional included GPU plugin for the +Region Locker can tint the locked areas gray, but is incompatible with other GPU plugins such as 117's HD OSRS. +If you choose not to include it, the world map will show locked and unlocked regions instead. + +In order to access a region, you will need to access it entirely through unlocked regions. At no point are you +ever allowed to cross through locked regions, with the following exceptions: +* If your starting region is not Lumbridge, when you complete Tutorial Island, you will need to traverse locked regions +to reach your intended starting location. +* If your starting region is not Lumbridge, you are allowed to "Home Teleport" to your starting region by using the +Lumbridge Home Teleport Spell and then walking to your start location. This is to prevent you from getting "stuck" after +using one-way transportation such as the Port Sarim Jail Teleport from Shantay Pass and being locked out of progression. +* All of your starting Tutorial Island items are assumed to be available at all times. If you have lost an important +item such as a Tinderbox, and cannot re-obtain it in your unlocked region, you are allowed to enter locked regions to +replace it in the least obtrusive way possible. +* If you need to adjust Group Ironman settings, such as adding or removing a member, you may freely access The Node +to do so. + +When passing through locked regions for such exceptions, do not interact with any NPCs, items, or enemies and attempt +to spend as little time in them as possible. + +The plugin will prevent equipping items that you have not unlocked the ability to wield. For example, attempting +to equip an Iron Platebody before the first Progressive Armor unlock will display a chat message and will not +equip the item. + +The plugin will show a list of your current tasks in the sidebar. The plugin will be able to detect the completion +of most tasks, but in the case that a task cannot be detected (for example, killing an enemy with no +drop table such as Deadly Red Spiders), the task can be marked as complete manually by clicking +on the button. This button can also be used to mark completed tasks you have done while playing OSRS mobile or +on a different client without having the plugin available. Simply click the button the next time you are logged in to +Runelite and connected to send the check. + +Due to the nature of randomizing a live MMO with no ability to freely edit the character or adjust game logic or +balancing, this randomizer relies heavily on **the honor system**. The plugin cannot prevent you from walking through +locked regions or equipping locked items with the plugin disabled before connecting. It is important +to acknowledge before starting that the entire purpose of the randomizer is a self-imposed challenge, and there +is little point in cheating by circumventing the plugin's restrictions or marking a task complete without actually +completing it. If you wish to play OSRS with no restrictions, that is always available without the plugin. + +In order to access the AP Text Client commands (such as `!hint` or to chat with other players in the seed), enter your +command in chat prefaced by the string `!ap`. Example commands: + +`!ap buying gf 100k` -> Sends the message "buying gf 100k" to the server +`!ap !hint Area: Lumbridge` -> Attempts to hint for the "Area: Lumbridge" item. Results will appear in your chat box. + +Other server messages, such as chat, will appear in your chat box, prefaced by the Archipelago icon. + +## What items and locations get shuffled? +Items: +- Every map region (at least one chunk but sometimes more) +- Weapon tiers from iron to Rune (bronze is available from the start) +- Armor tiers from iron to Rune (bronze is available from the start) +- Two Spell Tiers (bolt and blast spells) +- Three tiers of Ranged Armor (leather, studded leather + vambraces, green dragonhide) +- Three tiers of Ranged Weapons (oak, willow, maple bows and their respective highest tier of arrows) + +Locations: +* Every Quest is a location that will always be included in every seed +* A random assortment of tasks, separated into categories based on the skill required. +These task categories can have different weights, minimums, and maximums based on your options. + * For a full list of Locations, items, and regions, see the +[Logic Document](https://docs.google.com/spreadsheets/d/1R8Cm8L6YkRWeiN7uYrdru8Vc1DlJ0aFAinH_fwhV8aU/edit?usp=sharing) + +## Which items can be in another player's world? +Any item or region unlock can be found in any player's world. + +## What does another world's item look like in Old School Runescape? +Upon completing a task, the item and recipient will be listed in the player's chatbox. + +## When the player receives an item, what happens? +In addition to the message appearing in the chatbox, a UI window will appear listing the item and who sent it. +These boxes also appear when connecting to a seed already in progress to list the items you have acquired while offline. +The sidebar will list all received items below the task list, starting with regions, then showing the highest tier of +equipment in each category. \ No newline at end of file diff --git a/worlds/osrs/docs/setup_en.md b/worlds/osrs/docs/setup_en.md new file mode 100644 index 000000000000..47c1c8f16fd7 --- /dev/null +++ b/worlds/osrs/docs/setup_en.md @@ -0,0 +1,58 @@ +# Setup Guide for Old School Runescape + +## Required Software + +- [RuneLite](https://runelite.net/) +- If the account being used has been migrated to a Jagex Account, the [Jagex Launcher](https://www.jagex.com/en-GB/launcher) +will also be necessary to run RuneLite + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +You can customize your settings by visiting the +[Old School Runescape Player Options Page](/games/Old%20School%20Runescape/player-options). + +## Joining a MultiWorld Game + +### Install the RuneLite Plugins +Open RuneLite and click on the wrench icon on the right side. From there, click on the plug icon to access the +Plugin Hub. You will need to install the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago) +and [Region Locker Plugin](https://github.com/slaytostay/region-locker). The Region Locker plugin +will include three plugins; only the `Region Locker` plugin itself is required. The `Region Locker GPU` plugin can be +used to display locked chunks in gray, but is incompatible with other GPU plugins such as 117's HD OSRS and can be +disabled. + +### Create a new OSRS Account +The OSRS Randomizer assumes you are playing on a newly created f2p Ironman account. As such, you will need to [create a +new Runescape account](https://secure.runescape.com/m=account-creation/create_account?theme=oldschool). + +If you already have a [Jagex Account](https://www.jagex.com/en-GB/accounts) you can add up to 20 characters on +one account through the Jagex Launcher. Note that there is currently no way to _remove_ characters +from a Jagex Account, as such, you might want to create a separate account to hold your Archipelago +characters if you intend to use your main Jagex account for more characters in the future. + +**Protip**: In order to avoid having to remember random email addresses for many accounts, take advantage of an email +alias, a feature supported by most email providers. Any text after a `+` in your email address will redirect to your +normal address, but the email will be recognized by the Jagex login as a new email address. For example, if your email +were `Archipelago@gmail.com`, entering `Archipelago+OSRSRandomizer@gmail.com` would cause the confirmation email to +be sent to your primary address, but the alias can be used to create a new account. One recommendation would be to +include the date of generation in the account, such as `Archipelago+APYYMMDD@gmail.com` for easy memorability. + +After creating an account, you may run through Tutorial Island without connecting; the randomizer has no +effect on the Tutorial. + +### Connect to the Multiserver +In the Archipelago Plugin, enter your server information. The `Auto Reconnect on Login For` field should remain blank; +it will be populated by the character name you first connect with, and it will reconnect to the AP server whenever that +character logs in. Open the Archipelago panel on the right-hand side to connect to the multiworld while logged in to +a game world to associate this character to the randomizer. + +For further information about how to connect to the server in the RuneLite plugin, +please see the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago) instructions. \ No newline at end of file diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index 20111aa01d66..cf268509493c 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -35,17 +35,13 @@ def has_requirements_for_level_star( state: CollectionState, level: Overcooked2GenericLevel, stars: int, player: int) -> bool: assert 0 <= stars <= 3 - # First ensure that previous stars are obtainable - if stars > 1: - if not has_requirements_for_level_star(state, level, stars-1, player): - return False - - # Second, ensure that global requirements are met + # First, ensure that global requirements for this many stars are met. + # Lower numbers of stars are implied meetable if this level is meetable. if not meets_requirements(state, "*", stars, player): return False - # Finally, return success only if this level's requirements are met - return meets_requirements(state, level.shortname, stars, player) + # Then return success only if this level's requirements are met at all stars up through this one + return all(meets_requirements(state, level.shortname, s, player) for s in range(1, stars + 1)) def meets_requirements(state: CollectionState, name: str, stars: int, player: int): @@ -421,6 +417,7 @@ def can_reach_kevin_eight_island(state: CollectionState, player: int, allow_tric }, ), ( # 3-star + # Necessarily implies 2-star [ # Exclusive "Progressive Dash", "Spare Plate", diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 0437c0dae8ff..0dd874b25029 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -1,3 +1,28 @@ +# 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 rare issue where receiving a wonder trade could partially corrupt the save data, preventing the player from +receiving new items. +- Fixed the client spamming the "goal complete" status update to the server instead of sending it once. +- Fixed the `trainer_party_blacklist` option checking for the existence of the "_Legendaries" shortcut in the +`starter_blacklist` option instead of itself. +- 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. +- Prevent the poke flute sound from replacing the evolution fanfare, which would cause the game to wait in silence for +a long time during the evolution scene. + # 2.2.0 ### Features @@ -175,6 +200,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 aa4f6ccf7519..7b62b9ef73b1 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -15,11 +15,11 @@ from worlds.AutoWorld import WebWorld, World from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient -from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, data as emerald_data -from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, - offset_item_value) -from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map, - create_locations_with_tags, set_free_fly, set_legendary_cave_entrances) +from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, LocationCategory, data as emerald_data +from .groups import ITEM_GROUPS, LOCATION_GROUPS +from .items import PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, offset_item_value +from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_by_category, + set_free_fly, set_legendary_cave_entrances) from .opponents import randomize_opponent_parties from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions, RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement) @@ -52,8 +52,17 @@ class PokemonEmeraldWebWorld(WebWorld): "setup/es", ["nachocua"] ) + + setup_sv = Tutorial( + "Multivärld Installations Guide", + "En guide fÃļr att kunna spela PokÊmon Emerald med Archipelago.", + "Svenska", + "setup_sv.md", + "setup/sv", + ["Tsukino"] + ) - tutorials = [setup_en, setup_es] + tutorials = [setup_en, setup_es, setup_sv] class PokemonEmeraldSettings(settings.Group): @@ -124,9 +133,10 @@ def __init__(self, multiworld, player): @classmethod def stage_assert_generate(cls, multiworld: MultiWorld) -> None: - from .sanity_check import validate_regions + from .sanity_check import validate_regions, validate_group_maps assert validate_regions() + assert validate_group_maps() def get_filler_item_name(self) -> str: return "Great Ball" @@ -168,7 +178,7 @@ def generate_early(self) -> None: for species_name in self.options.trainer_party_blacklist.value if species_name != "_Legendaries" } - if "_Legendaries" in self.options.starter_blacklist.value: + if "_Legendaries" in self.options.trainer_party_blacklist.value: self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON # In race mode we don't patch any item location information into the ROM @@ -228,24 +238,32 @@ def generate_early(self) -> None: def create_regions(self) -> None: from .regions import create_regions - regions = create_regions(self) - - tags = {"Badge", "HM", "KeyItem", "Rod", "Bike", "EventTicket"} # Tags with progression items always included + all_regions = create_regions(self) + + # Categories with progression items always included + categories = { + LocationCategory.BADGE, + LocationCategory.HM, + LocationCategory.KEY, + LocationCategory.ROD, + LocationCategory.BIKE, + LocationCategory.TICKET + } if self.options.overworld_items: - tags.add("OverworldItem") + categories.add(LocationCategory.OVERWORLD_ITEM) if self.options.hidden_items: - tags.add("HiddenItem") + categories.add(LocationCategory.HIDDEN_ITEM) if self.options.npc_gifts: - tags.add("NpcGift") + categories.add(LocationCategory.GIFT) if self.options.berry_trees: - tags.add("BerryTree") + categories.add(LocationCategory.BERRY_TREE) if self.options.dexsanity: - tags.add("Pokedex") + categories.add(LocationCategory.POKEDEX) if self.options.trainersanity: - tags.add("Trainer") - create_locations_with_tags(self, regions, tags) + categories.add(LocationCategory.TRAINER) + create_locations_by_category(self, all_regions, categories) - self.multiworld.regions.extend(regions.values()) + self.multiworld.regions.extend(all_regions.values()) # Exclude locations which are always locked behind the player's goal def exclude_locations(location_names: List[str]): @@ -279,6 +297,12 @@ def exclude_locations(location_names: List[str]): "Safari Zone SE - Hidden Item in South Grass 2", "Safari Zone SE - Item in Grass", ]) + + # Sacred ash is on Navel Rock, which is locked behind the event tickets + if not self.options.event_tickets: + exclude_locations([ + "Navel Rock Top - Hidden Item Sacred Ash", + ]) elif self.options.goal == Goal.option_steven: exclude_locations([ "Meteor Falls 1F - Rival Steven", @@ -316,21 +340,21 @@ def create_items(self) -> None: # Filter progression items which shouldn't be shuffled into the itempool. # Their locations will still exist, but event items will be placed and # locked at their vanilla locations instead. - filter_tags = set() + filter_categories = set() if not self.options.key_items: - filter_tags.add("KeyItem") + filter_categories.add(LocationCategory.KEY) if not self.options.rods: - filter_tags.add("Rod") + filter_categories.add(LocationCategory.ROD) if not self.options.bikes: - filter_tags.add("Bike") + filter_categories.add(LocationCategory.BIKE) if not self.options.event_tickets: - filter_tags.add("EventTicket") + filter_categories.add(LocationCategory.TICKET) if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}: - filter_tags.add("Badge") + filter_categories.add(LocationCategory.BADGE) if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}: - filter_tags.add("HM") + filter_categories.add(LocationCategory.HM) # If Badges and HMs are set to the `shuffle` option, don't add them to # the normal item pool, but do create their items and save them and @@ -338,17 +362,17 @@ def create_items(self) -> None: if self.options.badges == RandomizeBadges.option_shuffle: self.badge_shuffle_info = [ (location, self.create_item_by_code(location.default_item_code)) - for location in [l for l in item_locations if "Badge" in l.tags] + for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.BADGE] ] if self.options.hms == RandomizeHms.option_shuffle: self.hm_shuffle_info = [ (location, self.create_item_by_code(location.default_item_code)) - for location in [l for l in item_locations if "HM" in l.tags] + for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.HM] ] # Filter down locations to actual items that will be filled and create # the itempool. - item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0] + item_locations = [location for location in item_locations if emerald_data.locations[location.key].category not in filter_categories] default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations] # Take the itempool as-is @@ -357,7 +381,8 @@ def create_items(self) -> None: # Recreate the itempool from random items elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced): - item_categories = ["Ball", "Heal", "Candy", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc", "Berry"] + item_categories = ["Ball", "Healing", "Rare Candy", "Vitamin", "Evolution Stone", + "Money", "TM", "Held", "Misc", "Berry"] # Count occurrences of types of vanilla items in pool item_category_counter = Counter() @@ -427,25 +452,26 @@ def generate_basic(self) -> None: # Key items which are considered in access rules but not randomized are converted to events and placed # in their vanilla locations so that the player can have them in their inventory for logic. - def convert_unrandomized_items_to_events(tag: str) -> None: + def convert_unrandomized_items_to_events(category: LocationCategory) -> None: for location in self.multiworld.get_locations(self.player): - if location.tags is not None and tag in location.tags: + assert isinstance(location, PokemonEmeraldLocation) + if location.key is not None and emerald_data.locations[location.key].category == category: location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code])) location.progress_type = LocationProgressType.DEFAULT location.address = None if self.options.badges == RandomizeBadges.option_vanilla: - convert_unrandomized_items_to_events("Badge") + convert_unrandomized_items_to_events(LocationCategory.BADGE) if self.options.hms == RandomizeHms.option_vanilla: - convert_unrandomized_items_to_events("HM") + convert_unrandomized_items_to_events(LocationCategory.HM) if not self.options.rods: - convert_unrandomized_items_to_events("Rod") + convert_unrandomized_items_to_events(LocationCategory.ROD) if not self.options.bikes: - convert_unrandomized_items_to_events("Bike") + convert_unrandomized_items_to_events(LocationCategory.BIKE) if not self.options.event_tickets: - convert_unrandomized_items_to_events("EventTicket") + convert_unrandomized_items_to_events(LocationCategory.TICKET) if not self.options.key_items: - convert_unrandomized_items_to_events("KeyItem") + convert_unrandomized_items_to_events(LocationCategory.KEY) def pre_fill(self) -> None: # Badges and HMs that are set to shuffle need to be placed at @@ -609,21 +635,34 @@ def write_spoiler(self, spoiler_handle: TextIO): spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n") + slot_to_rod_suffix = { + 0: " (Old Rod)", + 1: " (Old Rod)", + 2: " (Good Rod)", + 3: " (Good Rod)", + 4: " (Good Rod)", + 5: " (Super Rod)", + 6: " (Super Rod)", + 7: " (Super Rod)", + 8: " (Super Rod)", + 9: " (Super Rod)", + } + species_maps = defaultdict(set) for map in self.modified_maps.values(): if map.land_encounters is not None: for encounter in map.land_encounters.slots: - species_maps[encounter].add(map.name[4:]) + species_maps[encounter].add(map.label + " (Land)") if map.water_encounters is not None: for encounter in map.water_encounters.slots: - species_maps[encounter].add(map.name[4:]) + species_maps[encounter].add(map.label + " (Water)") if map.fishing_encounters is not None: - for encounter in map.fishing_encounters.slots: - species_maps[encounter].add(map.name[4:]) + for slot, encounter in enumerate(map.fishing_encounters.slots): + species_maps[encounter].add(map.label + slot_to_rod_suffix[slot]) - lines = [f"{emerald_data.species[species].label}: {', '.join(maps)}\n" + lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n" for species, maps in species_maps.items()] lines.sort() for line in lines: @@ -635,35 +674,35 @@ def extend_hint_information(self, hint_data): if self.options.dexsanity: from collections import defaultdict - slot_to_rod = { - 0: "_OLD_ROD", - 1: "_OLD_ROD", - 2: "_GOOD_ROD", - 3: "_GOOD_ROD", - 4: "_GOOD_ROD", - 5: "_SUPER_ROD", - 6: "_SUPER_ROD", - 7: "_SUPER_ROD", - 8: "_SUPER_ROD", - 9: "_SUPER_ROD", + slot_to_rod_suffix = { + 0: " (Old Rod)", + 1: " (Old Rod)", + 2: " (Good Rod)", + 3: " (Good Rod)", + 4: " (Good Rod)", + 5: " (Super Rod)", + 6: " (Super Rod)", + 7: " (Super Rod)", + 8: " (Super Rod)", + 9: " (Super Rod)", } species_maps = defaultdict(set) for map in self.modified_maps.values(): if map.land_encounters is not None: for encounter in map.land_encounters.slots: - species_maps[encounter].add(map.name[4:] + "_GRASS") + species_maps[encounter].add(map.label + " (Land)") if map.water_encounters is not None: for encounter in map.water_encounters.slots: - species_maps[encounter].add(map.name[4:] + "_WATER") + species_maps[encounter].add(map.label + " (Water)") if map.fishing_encounters is not None: for slot, encounter in enumerate(map.fishing_encounters.slots): - species_maps[encounter].add(map.name[4:] + slot_to_rod[slot]) + species_maps[encounter].add(map.label + slot_to_rod_suffix[slot]) hint_data[self.player] = { - self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(maps) + self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(sorted(maps)) for species, maps in species_maps.items() } @@ -702,6 +741,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 a830957e9c7e..5add7b3fca40 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -117,11 +117,17 @@ DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()} CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()} +SHOAL_CAVE_MAPS = tuple(data.constants[map_name] for map_name in [ + "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM", +]) + 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,13 +138,15 @@ 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 ignore_next_death_link: bool - def __init__(self) -> None: - super().__init__() + current_map: Optional[int] + + def initialize_client(self): self.local_checked_locations = set() self.local_set_events = {} self.local_found_key_items = {} @@ -150,6 +158,8 @@ def __init__(self) -> None: self.death_counter = 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 @@ -179,9 +189,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 @@ -243,6 +251,7 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little") sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little") + await self.handle_tracker_info(ctx, guards) await self.handle_death_link(ctx, guards) await self.handle_received_items(ctx, guards) await self.handle_wonder_trade(ctx, guards) @@ -348,6 +357,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, @@ -403,6 +413,35 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # Exit handler and return to main loop to reconnect pass + async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None: + # Current map + sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little") + + read_result = await bizhawk.guarded_read( + ctx.bizhawk_ctx, + [ + (sb1_address + 0x4, 2, "System Bus"), # Current map + (sb1_address + 0x1450 + (data.constants["FLAG_SYS_SHOAL_TIDE"] // 8), 1, "System Bus"), + ], + [guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]] + ) + if read_result is None: # Save block moved + return + + current_map = int.from_bytes(read_result[0], "big") + shoal_cave = int(read_result[1][0] & (1 << (data.constants["FLAG_SYS_SHOAL_TIDE"] % 8)) > 0) + if current_map != self.current_map: + self.current_map = current_map + await ctx.send_msgs([{ + "cmd": "Bounce", + "slots": [ctx.slot], + "data": { + "type": "MapUpdate", + "mapId": current_map, + **({"tide": shoal_cave} if current_map in SHOAL_CAVE_MAPS else {}), + }, + }]) + async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None: """ Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game @@ -516,28 +555,36 @@ async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[st if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2: # Game has wonder trade data to send. Send it to data storage, remove it from the game's memory, # and mark that the game is waiting on receiving a trade - Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data))) - await bizhawk.write(ctx.bizhawk_ctx, [ + success = await bizhawk.guarded_write(ctx.bizhawk_ctx, [ (sb1_address + 0x377C, bytes(0x50), "System Bus"), (sb1_address + 0x37CC, [1], "System Bus"), - ]) + ], [guards["SAVE BLOCK 1"]]) + if success: + Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data))) 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..cd1becf44b22 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -117,6 +117,21 @@ class ItemData(NamedTuple): tags: FrozenSet[str] +class LocationCategory(IntEnum): + BADGE = 0 + HM = 1 + KEY = 2 + ROD = 3 + BIKE = 4 + TICKET = 5 + OVERWORLD_ITEM = 6 + HIDDEN_ITEM = 7 + GIFT = 8 + BERRY_TREE = 9 + TRAINER = 10 + POKEDEX = 11 + + class LocationData(NamedTuple): name: str label: str @@ -124,6 +139,7 @@ class LocationData(NamedTuple): default_item: int address: Union[int, List[int]] flag: int + category: LocationCategory tags: FrozenSet[str] @@ -135,6 +151,7 @@ class EncounterTableData(NamedTuple): @dataclass class MapData: name: str + label: str header_address: int land_encounters: Optional[EncounterTableData] water_encounters: Optional[EncounterTableData] @@ -276,15 +293,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 @@ -343,6 +358,8 @@ def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]: def _init() -> None: + import re + extracted_data: Dict[str, Any] = load_json_data("extracted_data.json") data.constants = extracted_data["constants"] data.ram_addresses = extracted_data["misc_ram_addresses"] @@ -352,6 +369,7 @@ def _init() -> None: # Create map data for map_name, map_json in extracted_data["maps"].items(): + assert isinstance(map_name, str) if map_name in IGNORABLE_MAPS: continue @@ -375,8 +393,35 @@ def _init() -> None: map_json["fishing_encounters"]["address"] ) + # Derive a user-facing label + label = [] + for word in map_name[4:].split("_"): + # 1F, B1F, 2R, etc. + re_match = re.match(r"^B?\d+[FRP]$", word) + if re_match: + label.append(word) + continue + + # Route 103, Hall 1, House 5, etc. + re_match = re.match(r"^([A-Z]+)(\d+)$", word) + if re_match: + label.append(re_match.group(1).capitalize()) + label.append(re_match.group(2).lstrip("0")) + continue + + if word == "OF": + label.append("of") + continue + + if word == "SS": + label.append("S.S.") + continue + + label.append(word.capitalize()) + data.maps[map_name] = MapData( map_name, + " ".join(label), map_json["header_address"], land_encounters, water_encounters, @@ -433,6 +478,7 @@ def _init() -> None: location_json["default_item"], [location_json["address"]] + [j["address"] for j in alternate_rival_jsons], location_json["flag"], + LocationCategory[location_attributes_json[location_name]["category"]], frozenset(location_attributes_json[location_name]["tags"]) ) else: @@ -443,6 +489,7 @@ def _init() -> None: location_json["default_item"], location_json["address"], location_json["flag"], + LocationCategory[location_attributes_json[location_name]["category"]], frozenset(location_attributes_json[location_name]["tags"]) ) new_region.locations.append(location_name) @@ -950,6 +997,7 @@ def _init() -> None: evo_stage_to_ball_map[evo_stage], data.locations[dex_location_name].address, data.locations[dex_location_name].flag, + data.locations[dex_location_name].category, data.locations[dex_location_name].tags ) @@ -1411,9 +1459,6 @@ def _init() -> None: for warp, destination in extracted_data["warps"].items(): data.warp_map[warp] = None if destination == "" else destination - if encoded_warp not in data.warp_map: - data.warp_map[encoded_warp] = None - # Create trainer data for i, trainer_json in enumerate(extracted_data["trainers"]): party_json = trainer_json["party"] diff --git a/worlds/pokemon_emerald/data/items.json b/worlds/pokemon_emerald/data/items.json index 139d75aad0ab..4c09d215cf3c 100644 --- a/worlds/pokemon_emerald/data/items.json +++ b/worlds/pokemon_emerald/data/items.json @@ -52,49 +52,49 @@ "ITEM_HM_CUT": { "label": "HM01 Cut", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM01", "Unique"], "modern_id": 420 }, "ITEM_HM_FLY": { "label": "HM02 Fly", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM02", "Unique"], "modern_id": 421 }, "ITEM_HM_SURF": { "label": "HM03 Surf", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM03", "Unique"], "modern_id": 422 }, "ITEM_HM_STRENGTH": { "label": "HM04 Strength", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM04", "Unique"], "modern_id": 423 }, "ITEM_HM_FLASH": { "label": "HM05 Flash", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM05", "Unique"], "modern_id": 424 }, "ITEM_HM_ROCK_SMASH": { "label": "HM06 Rock Smash", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM06", "Unique"], "modern_id": 425 }, "ITEM_HM_WATERFALL": { "label": "HM07 Waterfall", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM07", "Unique"], "modern_id": 737 }, "ITEM_HM_DIVE": { "label": "HM08 Dive", "classification": "PROGRESSION", - "tags": ["HM", "Unique"], + "tags": ["HM", "HM08", "Unique"], "modern_id": null }, @@ -375,169 +375,169 @@ "ITEM_POTION": { "label": "Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 17 }, "ITEM_ANTIDOTE": { "label": "Antidote", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 18 }, "ITEM_BURN_HEAL": { "label": "Burn Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 19 }, "ITEM_ICE_HEAL": { "label": "Ice Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 20 }, "ITEM_AWAKENING": { "label": "Awakening", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 21 }, "ITEM_PARALYZE_HEAL": { "label": "Paralyze Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 22 }, "ITEM_FULL_RESTORE": { "label": "Full Restore", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 23 }, "ITEM_MAX_POTION": { "label": "Max Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 24 }, "ITEM_HYPER_POTION": { "label": "Hyper Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 25 }, "ITEM_SUPER_POTION": { "label": "Super Potion", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 26 }, "ITEM_FULL_HEAL": { "label": "Full Heal", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 27 }, "ITEM_REVIVE": { "label": "Revive", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 28 }, "ITEM_MAX_REVIVE": { "label": "Max Revive", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 29 }, "ITEM_FRESH_WATER": { "label": "Fresh Water", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 30 }, "ITEM_SODA_POP": { "label": "Soda Pop", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 31 }, "ITEM_LEMONADE": { "label": "Lemonade", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 32 }, "ITEM_MOOMOO_MILK": { "label": "Moomoo Milk", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 33 }, "ITEM_ENERGY_POWDER": { "label": "Energy Powder", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 34 }, "ITEM_ENERGY_ROOT": { "label": "Energy Root", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 35 }, "ITEM_HEAL_POWDER": { "label": "Heal Powder", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 36 }, "ITEM_REVIVAL_HERB": { "label": "Revival Herb", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 37 }, "ITEM_ETHER": { "label": "Ether", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 38 }, "ITEM_MAX_ETHER": { "label": "Max Ether", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 39 }, "ITEM_ELIXIR": { "label": "Elixir", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 40 }, "ITEM_MAX_ELIXIR": { "label": "Max Elixir", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 41 }, "ITEM_LAVA_COOKIE": { "label": "Lava Cookie", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 42 }, "ITEM_BERRY_JUICE": { "label": "Berry Juice", "classification": "FILLER", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 43 }, "ITEM_SACRED_ASH": { "label": "Sacred Ash", "classification": "USEFUL", - "tags": ["Heal"], + "tags": ["Healing"], "modern_id": 44 }, @@ -736,19 +736,19 @@ }, "ITEM_BLACK_FLUTE": { "label": "Black Flute", - "classification": "FILLER", + "classification": "USEFUL", "tags": ["Misc"], "modern_id": 68 }, "ITEM_WHITE_FLUTE": { "label": "White Flute", - "classification": "FILLER", + "classification": "USEFUL", "tags": ["Misc"], "modern_id": 69 }, "ITEM_HEART_SCALE": { "label": "Heart Scale", - "classification": "FILLER", + "classification": "USEFUL", "tags": ["Misc"], "modern_id": 93 }, @@ -757,37 +757,37 @@ "ITEM_SUN_STONE": { "label": "Sun Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 80 }, "ITEM_MOON_STONE": { "label": "Moon Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 81 }, "ITEM_FIRE_STONE": { "label": "Fire Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 82 }, "ITEM_THUNDER_STONE": { "label": "Thunder Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 83 }, "ITEM_WATER_STONE": { "label": "Water Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 84 }, "ITEM_LEAF_STONE": { "label": "Leaf Stone", "classification": "USEFUL", - "tags": ["EvoStone"], + "tags": ["Evolution Stone"], "modern_id": 85 }, @@ -1215,7 +1215,7 @@ "ITEM_KINGS_ROCK": { "label": "King's Rock", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 221 }, "ITEM_SILVER_POWDER": { @@ -1245,13 +1245,13 @@ "ITEM_DEEP_SEA_TOOTH": { "label": "Deep Sea Tooth", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 226 }, "ITEM_DEEP_SEA_SCALE": { "label": "Deep Sea Scale", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 227 }, "ITEM_SMOKE_BALL": { @@ -1287,7 +1287,7 @@ "ITEM_METAL_COAT": { "label": "Metal Coat", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 233 }, "ITEM_LEFTOVERS": { @@ -1299,7 +1299,7 @@ "ITEM_DRAGON_SCALE": { "label": "Dragon Scale", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 235 }, "ITEM_LIGHT_BALL": { @@ -1401,7 +1401,7 @@ "ITEM_UP_GRADE": { "label": "Up-Grade", "classification": "USEFUL", - "tags": ["Held"], + "tags": ["Held", "Evolution Stone"], "modern_id": 252 }, "ITEM_SHELL_BELL": { diff --git a/worlds/pokemon_emerald/data/locations.json b/worlds/pokemon_emerald/data/locations.json index 55ef15d871bb..63f42340cce4 100644 --- a/worlds/pokemon_emerald/data/locations.json +++ b/worlds/pokemon_emerald/data/locations.json @@ -1,5364 +1,6702 @@ { "BADGE_1": { "label": "Rustboro Gym - Stone Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_2": { "label": "Dewford Gym - Knuckle Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_3": { "label": "Mauville Gym - Dynamo Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_4": { "label": "Lavaridge Gym - Heat Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_5": { "label": "Petalburg Gym - Balance Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_6": { "label": "Fortree Gym - Feather Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_7": { "label": "Mossdeep Gym - Mind Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "BADGE_8": { "label": "Sootopolis Gym - Rain Badge", - "tags": ["Badge"] + "tags": [], + "category": "BADGE" }, "NPC_GIFT_RECEIVED_HM_CUT": { "label": "Rustboro City - HM01 from Cutter's House", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_FLY": { "label": "Route 119 - HM02 from Rival Battle", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_SURF": { "label": "Petalburg City - HM03 from Wally's Uncle", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_STRENGTH": { "label": "Rusturf Tunnel - HM04 from Tunneler", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_FLASH": { "label": "Granite Cave 1F - HM05 from Hiker", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_ROCK_SMASH": { "label": "Mauville City - HM06 from Rock Smash Guy", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_WATERFALL": { "label": "Sootopolis City - HM07 from Wallace", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_HM_DIVE": { "label": "Mossdeep City - HM08 from Steven's House", - "tags": ["HM"] + "tags": [], + "category": "HM" }, "NPC_GIFT_RECEIVED_ACRO_BIKE": { "label": "Mauville City - Acro Bike", - "tags": ["Bike"] + "tags": [], + "category": "BIKE" }, "NPC_GIFT_RECEIVED_MACH_BIKE": { "label": "Mauville City - Mach Bike", - "tags": ["Bike"] + "tags": [], + "category": "BIKE" }, "NPC_GIFT_RECEIVED_WAILMER_PAIL": { "label": "Route 104 - Wailmer Pail from Flower Shop Lady", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL": { "label": "Rusturf Tunnel - Recover Devon Goods", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_LETTER": { "label": "Devon Corp 3F - Letter from Mr. Stone", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_COIN_CASE": { "label": "Mauville City - Coin Case from Lady in House", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_METEORITE": { "label": "Mt Chimney - Meteorite from Machine", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_GO_GOGGLES": { "label": "Lavaridge Town - Go Goggles from Rival", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON": { "label": "Mauville City - Basement Key from Wattson", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_ITEMFINDER": { "label": "Route 110 - Itemfinder from Rival", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_DEVON_SCOPE": { "label": "Route 120 - Devon Scope from Steven", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_MAGMA_EMBLEM": { "label": "Mt Pyre Summit - Magma Emblem from Old Lady", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY": { "label": "Abandoned Ship - Captain's Office Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY": { "label": "Abandoned Ship HF - Room 4 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY": { "label": "Abandoned Ship HF - Room 1 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY": { "label": "Abandoned Ship HF - Room 6 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY": { "label": "Abandoned Ship HF - Room 2 Key", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER": { "label": "Abandoned Ship HF - Scanner", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_POKEBLOCK_CASE": { "label": "Lilycove City - Pokeblock Case from Contest Hall", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_SS_TICKET": { "label": "Littleroot Town - S.S. Ticket from Norman", - "tags": ["KeyItem"] + "tags": [], + "category": "KEY" }, "NPC_GIFT_RECEIVED_AURORA_TICKET": { "label": "Littleroot Town - Aurora Ticket from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_EON_TICKET": { "label": "Littleroot Town - Eon Ticket from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_MYSTIC_TICKET": { "label": "Littleroot Town - Mystic Ticket from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_OLD_SEA_MAP": { "label": "Littleroot Town - Old Sea Map from Norman", - "tags": ["EventTicket"] + "tags": [], + "category": "TICKET" }, "NPC_GIFT_RECEIVED_OLD_ROD": { "label": "Dewford Town - Old Rod from Fisherman", - "tags": ["Rod"] + "tags": [], + "category": "ROD" }, "NPC_GIFT_RECEIVED_GOOD_ROD": { "label": "Route 118 - Good Rod from Fisherman", - "tags": ["Rod"] + "tags": [], + "category": "ROD" }, "NPC_GIFT_RECEIVED_SUPER_ROD": { "label": "Mossdeep City - Super Rod from Fisherman in House", - "tags": ["Rod"] + "tags": [], + "category": "ROD" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM": { "label": "Artisan Cave B1F - Hidden Item 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON": { "label": "Artisan Cave B1F - Hidden Item 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN": { "label": "Artisan Cave B1F - Hidden Item 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC": { "label": "Artisan Cave B1F - Hidden Item 4", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_FALLARBOR_TOWN_NUGGET": { "label": "Fallarbor Town - Hidden Item in Crater", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_1": { "label": "Granite Cave B2F - Hidden Item After Crumbling Floor", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_GRANITE_CAVE_B2F_EVERSTONE_2": { "label": "Granite Cave B2F - Hidden Item on Platform", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_JAGGED_PASS_FULL_HEAL": { "label": "Jagged Pass - Hidden Item in Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_JAGGED_PASS_GREAT_BALL": { "label": "Jagged Pass - Hidden Item in Corner", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LAVARIDGE_TOWN_ICE_HEAL": { "label": "Lavaridge Town - Hidden Item in Springs", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LILYCOVE_CITY_HEART_SCALE": { "label": "Lilycove City - Hidden Item on Beach West", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LILYCOVE_CITY_POKE_BALL": { "label": "Lilycove City - Hidden Item on Beach East", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_LILYCOVE_CITY_PP_UP": { "label": "Lilycove City - Hidden Item on Beach North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_EXTERIOR_MAX_ETHER": { "label": "Mt Pyre Exterior - Hidden Item First Grave", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_EXTERIOR_ULTRA_BALL": { "label": "Mt Pyre Exterior - Hidden Item Second Grave", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_SUMMIT_RARE_CANDY": { "label": "Mt Pyre Summit - Hidden Item in Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_MT_PYRE_SUMMIT_ZINC": { "label": "Mt Pyre Summit - Hidden Item Grave", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_CITY_RARE_CANDY": { "label": "Petalburg City - Hidden Item Past Pond South", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_POKE_BALL": { "label": "Petalburg Woods - Hidden Item After Grunt", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_POTION": { "label": "Petalburg Woods - Hidden Item Southeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_1": { "label": "Petalburg Woods - Hidden Item Past Tree North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_PETALBURG_WOODS_TINY_MUSHROOM_2": { "label": "Petalburg Woods - Hidden Item Past Tree South", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_ANTIDOTE": { "label": "Route 104 - Hidden Item on Beach 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_HEART_SCALE": { "label": "Route 104 - Hidden Item on Beach 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_POTION": { "label": "Route 104 - Hidden Item on Beach 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_POKE_BALL": { "label": "Route 104 - Hidden Item Behind Flower Shop 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_104_SUPER_POTION": { "label": "Route 104 - Hidden Item Behind Flower Shop 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_105_BIG_PEARL": { "label": "Route 105 - Hidden Item Between Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_105_HEART_SCALE": { "label": "Route 105 - Hidden Item on Small Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_106_HEART_SCALE": { "label": "Route 106 - Hidden Item on Beach 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_106_STARDUST": { "label": "Route 106 - Hidden Item on Beach 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_106_POKE_BALL": { "label": "Route 106 - Hidden Item on Beach 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_108_RARE_CANDY": { "label": "Route 108 - Hidden Item on Rock", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_REVIVE": { "label": "Route 109 - Hidden Item on Beach Southwest", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_ETHER": { "label": "Route 109 - Hidden Item on Beach Southeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_2": { "label": "Route 109 - Hidden Item on Beach Under Umbrella", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_GREAT_BALL": { "label": "Route 109 - Hidden Item on Beach West", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_1": { "label": "Route 109 - Hidden Item on Beach Behind Old Man", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_109_HEART_SCALE_3": { "label": "Route 109 - Hidden Item in Front of Couple", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_FULL_HEAL": { "label": "Route 110 - Hidden Item South of Rival", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_GREAT_BALL": { "label": "Route 110 - Hidden Item North of Rival", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_REVIVE": { "label": "Route 110 - Hidden Item Behind Two Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_110_POKE_BALL": { "label": "Route 110 - Hidden Item South of Berries", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_111_PROTEIN": { "label": "Route 111 - Hidden Item Desert Behind Tower", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_111_RARE_CANDY": { "label": "Route 111 - Hidden Item Desert on Rock 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_111_STARDUST": { "label": "Route 111 - Hidden Item Desert on Rock 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_113_ETHER": { "label": "Route 113 - Hidden Item Mound West of Three Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_113_NUGGET": { "label": "Route 113 - Hidden Item Mound Between Trainers", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_113_TM_DOUBLE_TEAM": { "label": "Route 113 - Hidden Item Mound West of Workshop", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_114_CARBOS": { "label": "Route 114 - Hidden Item Rock in Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_114_REVIVE": { "label": "Route 114 - Hidden Item West of Bridge", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_115_HEART_SCALE": { "label": "Route 115 - Hidden Item Behind Trainer on Beach", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_116_BLACK_GLASSES": { "label": "Route 116 - Hidden Item in East", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_116_SUPER_POTION": { "label": "Route 116 - Hidden Item in Tree Maze", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_117_REPEL": { "label": "Route 117 - Hidden Item Behind Flower Patch", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_118_HEART_SCALE": { "label": "Route 118 - Hidden Item West on Rock", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_118_IRON": { "label": "Route 118 - Hidden Item East on Rock", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_FULL_HEAL": { "label": "Route 119 - Hidden Item in South Tall Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_CALCIUM": { "label": "Route 119 - Hidden Item Across South Rail", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_ULTRA_BALL": { "label": "Route 119 - Hidden Item in East Tall Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_119_MAX_ETHER": { "label": "Route 119 - Hidden Item Next to Waterfall", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_1": { "label": "Route 120 - Hidden Item Behind Trees", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_REVIVE": { "label": "Route 120 - Hidden Item in North Tall Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_ZINC": { "label": "Route 120 - Hidden Item in Tall Grass Maze", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2": { "label": "Route 120 - Hidden Item Behind Southwest Pool", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_HP_UP": { "label": "Route 121 - Hidden Item West of Grunts", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_FULL_HEAL": { "label": "Route 121 - Hidden Item in Maze 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_MAX_REVIVE": { "label": "Route 121 - Hidden Item in Maze 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_121_NUGGET": { "label": "Route 121 - Hidden Item Behind Tree", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_PP_UP": { "label": "Route 123 - Hidden Item East Behind Tree 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_RARE_CANDY": { "label": "Route 123 - Hidden Item East Behind Tree 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_HYPER_POTION": { "label": "Route 123 - Hidden Item on Rock Before Ledges", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_SUPER_REPEL": { "label": "Route 123 - Hidden Item in North Path Grass", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_123_REVIVE": { "label": "Route 123 - Hidden Item Behind House", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_1": { "label": "Route 128 - Hidden Item North Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_2": { "label": "Route 128 - Hidden Item Center Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_ROUTE_128_HEART_SCALE_3": { "label": "Route 128 - Hidden Item Southwest Island", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_ZINC": { "label": "Safari Zone NE - Hidden Item North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_NORTH_EAST_RARE_CANDY": { "label": "Safari Zone NE - Hidden Item East", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_FULL_RESTORE": { "label": "Safari Zone SE - Hidden Item in South Grass 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SAFARI_ZONE_SOUTH_EAST_PP_UP": { "label": "Safari Zone SE - Hidden Item in South Grass 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_SS_TIDAL_LOWER_DECK_LEFTOVERS": { "label": "SS Tidal - Hidden Item in Lower Deck Trash Can", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_GREEN_SHARD": { "label": "Route 124 UW - Hidden Item in Big Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_CARBOS": { "label": "Route 124 UW - Hidden Item in Tunnel Alcove", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_CALCIUM": { "label": "Route 124 UW - Hidden Item in North Tunnel 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_2": { "label": "Route 124 UW - Hidden Item in North Tunnel 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_PEARL": { "label": "Route 124 UW - Hidden Item in Small Area North", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_BIG_PEARL": { "label": "Route 124 UW - Hidden Item in Small Area Middle", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_124_HEART_SCALE_1": { "label": "Route 124 UW - Hidden Item in Small Area South", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_STARDUST": { "label": "Route 126 UW - Hidden Item Northeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_ULTRA_BALL": { "label": "Route 126 UW - Hidden Item in North Alcove", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_BIG_PEARL": { "label": "Route 126 UW - Hidden Item in Southeast", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_HEART_SCALE": { "label": "Route 126 UW - Hidden Item in Northwest Alcove", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_BLUE_SHARD": { "label": "Route 126 UW - Hidden Item in Southwest Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_IRON": { "label": "Route 126 UW - Hidden Item in West Area 1", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_PEARL": { "label": "Route 126 UW - Hidden Item in West Area 2", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_126_YELLOW_SHARD": { "label": "Route 126 UW - Hidden Item in West Area 3", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_STAR_PIECE": { "label": "Route 127 UW - Hidden Item in West Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_HEART_SCALE": { "label": "Route 127 UW - Hidden Item in Center Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_HP_UP": { "label": "Route 127 UW - Hidden Item in East Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_127_RED_SHARD": { "label": "Route 127 UW - Hidden Item in Northeast Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_128_PEARL": { "label": "Route 128 UW - Hidden Item in East Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_UNDERWATER_128_PROTEIN": { "label": "Route 128 UW - Hidden Item in Small Area", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_VICTORY_ROAD_1F_ULTRA_BALL": { "label": "Victory Road 1F - Hidden Item on Southeast Ledge", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_VICTORY_ROAD_B2F_ELIXIR": { "label": "Victory Road B2F - Hidden Item Above Waterfall", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_VICTORY_ROAD_B2F_MAX_REPEL": { "label": "Victory Road B2F - Hidden Item in Northeast Corner", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH": { "label": "Navel Rock Top - Hidden Item Sacred Ash", - "tags": ["HiddenItem"] + "tags": [], + "category": "HIDDEN_ITEM" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_1_TM_RAIN_DANCE": { "label": "Abandoned Ship HF - Item in Room 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_3_WATER_STONE": { "label": "Abandoned Ship HF - Item in Room 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_6_LUXURY_BALL": { "label": "Abandoned Ship HF - Item in Room 6", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_1F_HARBOR_MAIL": { "label": "Abandoned Ship 1F - Item in East Side Northwest Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_2_1F_REVIVE": { "label": "Abandoned Ship 1F - Item in West Side North Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_B1F_ESCAPE_ROPE": { "label": "Abandoned Ship B1F - Item in South Rooms", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_B1F_TM_ICE_BEAM": { "label": "Abandoned Ship B1F - Item in Storage Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ABANDONED_SHIP_ROOMS_2_B1F_DIVE_BALL": { "label": "Abandoned Ship B1F - Item in North Rooms", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B1F_MASTER_BALL": { "label": "Aqua Hideout B1F - Item in Center Room 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B1F_NUGGET": { "label": "Aqua Hideout B1F - Item in Center Room 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B1F_MAX_ELIXIR": { "label": "Aqua Hideout B1F - Item in East Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_AQUA_HIDEOUT_B2F_NEST_BALL": { "label": "Aqua Hideout B2F - Item in Long Hallway", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ARTISAN_CAVE_1F_CARBOS": { "label": "Artisan Cave 1F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ARTISAN_CAVE_B1F_HP_UP": { "label": "Artisan Cave B1F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_FIERY_PATH_FIRE_STONE": { "label": "Fiery Path - Item Behind Boulders 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_FIERY_PATH_TM_TOXIC": { "label": "Fiery Path - Item Behind Boulders 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_1F_ESCAPE_ROPE": { "label": "Granite Cave 1F - Item Before Ladder", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_B1F_POKE_BALL": { "label": "Granite Cave B1F - Item in Alcove", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_B2F_RARE_CANDY": { "label": "Granite Cave B2F - Item After Crumbling Floor", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_GRANITE_CAVE_B2F_REPEL": { "label": "Granite Cave B2F - Item After Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_JAGGED_PASS_BURN_HEAL": { "label": "Jagged Pass - Item Below Hideout", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_LILYCOVE_CITY_MAX_REPEL": { "label": "Lilycove City - Item on Peninsula", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_1F_RARE_CANDY": { "label": "Magma Hideout 1F - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_2F_2R_FULL_RESTORE": { "label": "Magma Hideout 2F - Item on West Platform", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_2F_2R_MAX_ELIXIR": { "label": "Magma Hideout 2F - Item on East Platform", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_3F_1R_NUGGET": { "label": "Magma Hideout 3F - Item Before Last Floor", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_3F_2R_PP_MAX": { "label": "Magma Hideout 3F - Item in Drill Room", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_3F_3R_ECAPE_ROPE": { "label": "Magma Hideout 3F - Item After Groudon", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAGMA_HIDEOUT_4F_MAX_REVIVE": { "label": "Magma Hideout 4F - Item Before Groudon", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MAUVILLE_CITY_X_SPEED": { "label": "Mauville City - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_FULL_HEAL": { "label": "Meteor Falls 1F - Item Northeast", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_MOON_STONE": { "label": "Meteor Falls 1F - Item West", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_PP_UP": { "label": "Meteor Falls 1F - Item Below Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_1F_1R_TM_IRON_TAIL": { "label": "Meteor Falls 1F - Item Before Steven's Cave", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_METEOR_FALLS_B1F_2R_TM_DRAGON_CLAW": { "label": "Meteor Falls B1F - Item in North Cave", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MOSSDEEP_CITY_NET_BALL": { "label": "Mossdeep City - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_2F_ULTRA_BALL": { "label": "Mt Pyre 2F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_3F_SUPER_REPEL": { "label": "Mt Pyre 3F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_4F_SEA_INCENSE": { "label": "Mt Pyre 4F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_5F_LAX_INCENSE": { "label": "Mt Pyre 5F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_6F_TM_SHADOW_BALL": { "label": "Mt Pyre 6F - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_EXTERIOR_TM_SKILL_SWAP": { "label": "Mt Pyre Exterior - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_MT_PYRE_EXTERIOR_MAX_POTION": { "label": "Mt Pyre Exterior - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_ESCAPE_ROPE": { "label": "New Mauville - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_PARALYZE_HEAL": { "label": "New Mauville - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_FULL_HEAL": { "label": "New Mauville - Item 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_THUNDER_STONE": { "label": "New Mauville - Item 4", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_NEW_MAUVILLE_ULTRA_BALL": { "label": "New Mauville - Item 5", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_CITY_ETHER": { "label": "Petalburg City - Item Past Pond South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_CITY_MAX_REVIVE": { "label": "Petalburg City - Item Past Pond North", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_ETHER": { "label": "Petalburg Woods - Item Northwest", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_PARALYZE_HEAL": { "label": "Petalburg Woods - Item Southwest", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_GREAT_BALL": { "label": "Petalburg Woods - Item Past Tree Northeast", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_PETALBURG_WOODS_X_ATTACK": { "label": "Petalburg Woods - Item Past Tree South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_102_POTION": { "label": "Route 102 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_103_GUARD_SPEC": { "label": "Route 103 - Item Near Berries", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_103_PP_UP": { "label": "Route 103 - Item in Tree Maze", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_POKE_BALL": { "label": "Route 104 - Item Near Briney on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_POTION": { "label": "Route 104 - Item Behind Flower Shop", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_X_ACCURACY": { "label": "Route 104 - Item Behind Tree", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_104_PP_UP": { "label": "Route 104 - Item East Past Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_105_IRON": { "label": "Route 105 - Item on Island", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_106_PROTEIN": { "label": "Route 106 - Item on West Beach", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_108_STAR_PIECE": { "label": "Route 108 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_109_POTION": { "label": "Route 109 - Item on Beach", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_109_PP_UP": { "label": "Route 109 - Item on Island", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_110_DIRE_HIT": { "label": "Route 110 - Item South of Rival", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_110_ELIXIR": { "label": "Route 110 - Item South of Berries", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_110_RARE_CANDY": { "label": "Route 110 - Item on Island", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_ELIXIR": { "label": "Route 111 - Item Near Winstrates", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_HP_UP": { "label": "Route 111 - Item West of Pond Near Winstrates", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_STARDUST": { "label": "Route 111 - Item Desert Near Tower", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_111_TM_SANDSTORM": { "label": "Route 111 - Item Desert South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_112_NUGGET": { "label": "Route 112 - Item on Ledges", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_113_SUPER_REPEL": { "label": "Route 113 - Item Past Three Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_113_MAX_ETHER": { "label": "Route 113 - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_113_HYPER_POTION": { "label": "Route 113 - Item Near Fallarbor South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_114_ENERGY_POWDER": { "label": "Route 114 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_114_PROTEIN": { "label": "Route 114 - Item Behind Smashable Rock", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_114_RARE_CANDY": { "label": "Route 114 - Item Above Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_SUPER_POTION": { "label": "Route 115 - Item on Beach", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_PP_UP": { "label": "Route 115 - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_GREAT_BALL": { "label": "Route 115 - Item Behind Smashable Rock", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_HEAL_POWDER": { "label": "Route 115 - Item North Near Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_TM_FOCUS_PUNCH": { "label": "Route 115 - Item Near Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_115_IRON": { "label": "Route 115 - Item Past Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_REPEL": { "label": "Route 116 - Item in Grass", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_X_SPECIAL": { "label": "Route 116 - Item Near Tunnel", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_POTION": { "label": "Route 116 - Item in Tree Maze 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_ETHER": { "label": "Route 116 - Item in Tree Maze 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_116_HP_UP": { "label": "Route 116 - Item in East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_117_GREAT_BALL": { "label": "Route 117 - Item Behind Flower Patch", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_117_REVIVE": { "label": "Route 117 - Item Behind Tree", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_118_HYPER_POTION": { "label": "Route 118 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_SUPER_REPEL": { "label": "Route 119 - Item in South Tall Grass 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_HYPER_POTION_1": { "label": "Route 119 - Item in South Tall Grass 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_ZINC": { "label": "Route 119 - Item Across River South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_HYPER_POTION_2": { "label": "Route 119 - Item Near Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_ELIXIR_1": { "label": "Route 119 - Item East of Mud Slope", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_ELIXIR_2": { "label": "Route 119 - Item on River Bank", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_LEAF_STONE": { "label": "Route 119 - Item Near South Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_NUGGET": { "label": "Route 119 - Item Above North Waterfall 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_119_RARE_CANDY": { "label": "Route 119 - Item Above North Waterfall 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_NEST_BALL": { "label": "Route 120 - Item Near North Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_REVIVE": { "label": "Route 120 - Item in North Puddles", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_NUGGET": { "label": "Route 120 - Item in Tall Grass Maze", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_HYPER_POTION": { "label": "Route 120 - Item in Tall Grass South", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_120_FULL_HEAL": { "label": "Route 120 - Item Behind Southwest Pool", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_121_ZINC": { "label": "Route 121 - Item Near Safari Zone", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_121_REVIVE": { "label": "Route 121 - Item in Maze 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_121_CARBOS": { "label": "Route 121 - Item in Maze 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_ULTRA_BALL": { "label": "Route 123 - Item Below Ledges", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_ELIXIR": { "label": "Route 123 - Item on Ledges 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_REVIVAL_HERB": { "label": "Route 123 - Item on Ledges 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_PP_UP": { "label": "Route 123 - Item on Ledges 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_123_CALCIUM": { "label": "Route 123 - Item on Ledges 4", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_124_RED_SHARD": { "label": "Route 124 - Item in Northwest Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_124_YELLOW_SHARD": { "label": "Route 124 - Item in Northeast Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_124_BLUE_SHARD": { "label": "Route 124 - Item in Southwest Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_125_BIG_PEARL": { "label": "Route 125 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_126_GREEN_SHARD": { "label": "Route 126 - Item in Separated Area", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_127_ZINC": { "label": "Route 127 - Item North", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_127_CARBOS": { "label": "Route 127 - Item East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_127_RARE_CANDY": { "label": "Route 127 - Item Between Trainers", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_132_PROTEIN": { "label": "Route 132 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_132_RARE_CANDY": { "label": "Route 132 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_133_BIG_PEARL": { "label": "Route 133 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_133_MAX_REVIVE": { "label": "Route 133 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_133_STAR_PIECE": { "label": "Route 133 - Item 3", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_134_CARBOS": { "label": "Route 134 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_ROUTE_134_STAR_PIECE": { "label": "Route 134 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_RUSTBORO_CITY_X_DEFEND": { "label": "Rustboro City - Item Behind Fences", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_RUSTURF_TUNNEL_POKE_BALL": { "label": "Rusturf Tunnel - Item West", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_RUSTURF_TUNNEL_MAX_ETHER": { "label": "Rusturf Tunnel - Item East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_NORTH_CALCIUM": { "label": "Safari Zone N - Item in Grass", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_NORTH_EAST_NUGGET": { "label": "Safari Zone NE - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_NORTH_WEST_TM_SOLAR_BEAM": { "label": "Safari Zone NW - Item Behind Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_SOUTH_EAST_BIG_PEARL": { "label": "Safari Zone SE - Item in Grass", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SAFARI_ZONE_SOUTH_WEST_MAX_REVIVE": { "label": "Safari Zone SW - Item Behind Pond", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SCORCHED_SLAB_TM_SUNNY_DAY": { "label": "Scorched Slab - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SEAFLOOR_CAVERN_ROOM_9_TM_EARTHQUAKE": { "label": "Seafloor Cavern Room 9 - Item Before Kyogre", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_ENTRANCE_BIG_PEARL": { "label": "Shoal Cave Entrance - Item on Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_ICE_ROOM_NEVER_MELT_ICE": { "label": "Shoal Cave Ice Room - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_ICE_ROOM_TM_HAIL": { "label": "Shoal Cave Ice Room - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_INNER_ROOM_RARE_CANDY": { "label": "Shoal Cave Inner Room - Item in Center", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_SHOAL_CAVE_STAIRS_ROOM_ICE_HEAL": { "label": "Shoal Cave Stairs Room - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL": { "label": "Trick House Puzzle 1 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL": { "label": "Trick House Puzzle 2 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL": { "label": "Trick House Puzzle 2 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL": { "label": "Trick House Puzzle 3 - Item 1", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL": { "label": "Trick House Puzzle 3 - Item 2", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL": { "label": "Trick House Puzzle 4 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL": { "label": "Trick House Puzzle 6 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL": { "label": "Trick House Puzzle 7 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL": { "label": "Trick House Puzzle 8 - Item", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_1F_MAX_ELIXIR": { "label": "Victory Road 1F - Item East", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_1F_PP_UP": { "label": "Victory Road 1F - Item on Southeast Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_B1F_FULL_RESTORE": { "label": "Victory Road B1F - Item Behind Boulders", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_B1F_TM_PSYCHIC": { "label": "Victory Road B1F - Item on Northeast Ledge", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "ITEM_VICTORY_ROAD_B2F_FULL_HEAL": { "label": "Victory Road B2F - Item Above Waterfall", - "tags": ["OverworldItem"] + "tags": [], + "category": "OVERWORLD_ITEM" }, "NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON": { "label": "Mauville City - TM24 from Wattson", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_6_SODA_POP": { "label": "Route 109 - Seashore House Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_AMULET_COIN": { "label": "Littleroot Town - Amulet Coin from Mom", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_CHARCOAL": { "label": "Lavaridge Town Herb Shop - Charcoal from Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_CHESTO_BERRY_ROUTE_104": { "label": "Route 104 - Gift from Woman Near Berries", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_CLEANSE_TAG": { "label": "Mt Pyre 1F - Cleanse Tag from Woman in NE Corner", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_EXP_SHARE": { "label": "Devon Corp 3F - Exp. Share from Mr. Stone", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_FOCUS_BAND": { "label": "Shoal Cave Lower Room - Focus Band from Black Belt", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_GREAT_BALL_PETALBURG_WOODS": { "label": "Petalburg Woods - Gift from Devon Employee", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_GREAT_BALL_RUSTBORO_CITY": { "label": "Rustboro City - Gift from Devon Employee", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_KINGS_ROCK": { "label": "Mossdeep City - King's Rock from Boy", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_MACHO_BRACE": { "label": "Route 111 - Winstrate Family Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_MENTAL_HERB": { "label": "Fortree City - Wingull Delivery Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_MIRACLE_SEED": { "label": "Petalburg Woods - Miracle Seed from Lady", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_POTION_OLDALE": { "label": "Oldale Town - Gift from Shop Tutorial", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_POWDER_JAR": { "label": "Slateport City - Powder Jar from Lady in Market", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_PREMIER_BALL_RUSTBORO": { "label": "Rustboro City - Gift from Boy in Apartments", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_QUICK_CLAW": { "label": "Rustboro City - Quick Claw from School Teacher", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_REPEAT_BALL": { "label": "Route 116 - Gift from Devon Researcher", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SECRET_POWER": { "label": "Route 111 - Secret Power from Man Near Tree", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SILK_SCARF": { "label": "Dewford Town - Silk Scarf from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SOFT_SAND": { "label": "Route 109 - Soft Sand from Tuber", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SOOT_SACK": { "label": "Route 113 - Soot Sack from Glass Blower", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SOOTHE_BELL": { "label": "Slateport City - Soothe Bell from Woman in Fan Club", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_SUN_STONE_MOSSDEEP": { "label": "Space Center - Gift from Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_WATER_PULSE": { "label": "Sootopolis Gym - TM03 from Juan", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_CALM_MIND": { "label": "Mossdeep Gym - TM04 from Tate and Liza", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_ROAR": { "label": "Route 114 - TM05 from Roaring Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_BULK_UP": { "label": "Dewford Gym - TM08 from Brawly", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_BULLET_SEED": { "label": "Route 104 - TM09 from Boy", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_HIDDEN_POWER": { "label": "Fortree City - TM10 from Hidden Power Lady", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_GIGA_DRAIN": { "label": "Route 123 - TM19 from Girl near Berries", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_FRUSTRATION": { "label": "Pacifidlog Town - TM21 from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_RETURN": { "label": "Fallarbor Town - TM27 from Cozmo", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_RETURN_2": { "label": "Pacifidlog Town - TM27 from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_DIG": { "label": "Route 114 - TM28 from Fossil Maniac's Brother", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_BRICK_BREAK": { "label": "Sootopolis City - TM31 from Black Belt in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_SHOCK_WAVE": { "label": "Mauville Gym - TM34 from Wattson", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_SLUDGE_BOMB": { "label": "Dewford Town - TM36 from Sludge Bomb Man", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_ROCK_TOMB": { "label": "Rustboro Gym - TM39 from Roxanne", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_AERIAL_ACE": { "label": "Fortree Gym - TM40 from Winona", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_TORMENT": { "label": "Slateport City - TM41 from Sailor in Battle Tent", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_FACADE": { "label": "Petalburg Gym - TM42 from Norman", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_REST": { "label": "Lilycove City - TM44 from Man in House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_ATTRACT": { "label": "Verdanturf Town - TM45 from Woman in Battle Tent", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_THIEF": { "label": "Oceanic Museum - TM46 from Aqua Grunt in Museum", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_STEEL_WING": { "label": "Granite Cave 1F - TM47 from Steven", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_SNATCH": { "label": "SS Tidal - TM49 from Thief", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TM_OVERHEAT": { "label": "Lavaridge Gym - TM50 from Flannery", - "tags": ["NpcGift"] + "tags": ["Gym TMs"], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_WHITE_HERB": { "label": "Route 104 - White Herb from Lady Near Flower Shop", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_FLOWER_SHOP_RECEIVED_BERRY": { "label": "Route 104 - Berry from Girl in Flower Shop", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_DEEP_SEA_SCALE": { "label": "Slateport City - Deep Sea Scale from Capt. Stern", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_DEEP_SEA_TOOTH": { "label": "Slateport City - Deep Sea Tooth from Capt. Stern", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_1": { "label": "Trick House Puzzle 1 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_2": { "label": "Trick House Puzzle 2 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_3": { "label": "Trick House Puzzle 3 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_4": { "label": "Trick House Puzzle 4 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_5": { "label": "Trick House Puzzle 5 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_6": { "label": "Trick House Puzzle 6 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_TRICK_HOUSE_REWARD_7": { "label": "Trick House Puzzle 7 - Reward", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_RECEIVED_FIRST_POKEBALLS": { "label": "Littleroot Town - Pokeballs from Rival", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_1": { "label": "Sootopolis City - Berry from Girl on Grass 1", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_SOOTOPOLIS_RECEIVED_BERRY_2": { "label": "Sootopolis City - Berry from Girl on Grass 2", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_ROUTE_111_RECEIVED_BERRY": { "label": "Route 111 - Berry from Girl Near Berry Trees", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_ROUTE_114_RECEIVED_BERRY": { "label": "Route 114 - Berry from Man Near House", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_ROUTE_120_RECEIVED_BERRY": { "label": "Route 120 - Berry from Lady Near Berry Trees", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_1": { "label": "Route 123 - Berry from Berry Master 1", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_BERRY_MASTER_RECEIVED_BERRY_2": { "label": "Route 123 - Berry from Berry Master 2", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_BERRY_MASTERS_WIFE": { "label": "Route 123 - Berry from Berry Master's Wife", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "NPC_GIFT_LILYCOVE_RECEIVED_BERRY": { "label": "Lilycove City - Berry from Gentleman Above Ledges", - "tags": ["NpcGift"] + "tags": [], + "category": "GIFT" }, "BERRY_TREE_01": { "label": "Route 102 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_02": { "label": "Route 102 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_03": { "label": "Route 104 - Berry Tree Flower Shop 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_04": { "label": "Route 104 - Berry Tree Flower Shop 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_05": { "label": "Route 103 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_06": { "label": "Route 103 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_07": { "label": "Route 103 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_08": { "label": "Route 104 - Berry Tree North 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_09": { "label": "Route 104 - Berry Tree North 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_10": { "label": "Route 104 - Berry Tree North 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_11": { "label": "Route 104 - Berry Tree South 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_12": { "label": "Route 104 - Berry Tree South 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_13": { "label": "Route 104 - Berry Tree South 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_14": { "label": "Route 123 - Berry Tree Berry Master 6", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_15": { "label": "Route 123 - Berry Tree Berry Master 7", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_16": { "label": "Route 110 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_17": { "label": "Route 110 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_18": { "label": "Route 110 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_19": { "label": "Route 111 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_20": { "label": "Route 111 - Berry Tree 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_21": { "label": "Route 112 - Berry Tree 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_22": { "label": "Route 112 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_23": { "label": "Route 112 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_24": { "label": "Route 112 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_25": { "label": "Route 116 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_26": { "label": "Route 116 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_27": { "label": "Route 117 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_28": { "label": "Route 117 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_29": { "label": "Route 117 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_30": { "label": "Route 123 - Berry Tree Berry Master 8", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_31": { "label": "Route 118 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_32": { "label": "Route 118 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_33": { "label": "Route 118 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_34": { "label": "Route 119 - Berry Tree North 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_35": { "label": "Route 119 - Berry Tree North 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_36": { "label": "Route 119 - Berry Tree North 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_37": { "label": "Route 120 - Berry Tree in Side Area 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_38": { "label": "Route 120 - Berry Tree in Side Area 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_39": { "label": "Route 120 - Berry Tree in Side Area 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_40": { "label": "Route 120 - Berry Tree South 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_41": { "label": "Route 120 - Berry Tree South 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_42": { "label": "Route 120 - Berry Tree South 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_43": { "label": "Route 120 - Berry Tree Pond 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_44": { "label": "Route 120 - Berry Tree Pond 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_45": { "label": "Route 120 - Berry Tree Pond 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_46": { "label": "Route 120 - Berry Tree Pond 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_47": { "label": "Route 121 - Berry Tree West 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_48": { "label": "Route 121 - Berry Tree West 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_49": { "label": "Route 121 - Berry Tree West 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_50": { "label": "Route 121 - Berry Tree West 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_51": { "label": "Route 121 - Berry Tree East 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_52": { "label": "Route 121 - Berry Tree East 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_53": { "label": "Route 121 - Berry Tree East 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_54": { "label": "Route 121 - Berry Tree East 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_55": { "label": "Route 115 - Berry Tree Behind Smashable Rock 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_56": { "label": "Route 115 - Berry Tree Behind Smashable Rock 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_57": { "label": "Route 123 - Berry Tree East 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_58": { "label": "Route 123 - Berry Tree Berry Master 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_59": { "label": "Route 123 - Berry Tree Berry Master 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_60": { "label": "Route 123 - Berry Tree Berry Master 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_61": { "label": "Route 123 - Berry Tree Berry Master 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_62": { "label": "Route 123 - Berry Tree East 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_63": { "label": "Route 123 - Berry Tree East 5", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_64": { "label": "Route 123 - Berry Tree East 6", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_65": { "label": "Route 123 - Berry Tree Berry Master 9", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_66": { "label": "Route 116 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_67": { "label": "Route 116 - Berry Tree 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_68": { "label": "Route 114 - Berry Tree 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_69": { "label": "Route 115 - Berry Tree North 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_70": { "label": "Route 115 - Berry Tree North 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_71": { "label": "Route 115 - Berry Tree North 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_72": { "label": "Route 123 - Berry Tree Berry Master 10", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_73": { "label": "Route 123 - Berry Tree Berry Master 11", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_74": { "label": "Route 123 - Berry Tree Berry Master 12", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_75": { "label": "Route 104 - Berry Tree Flower Shop 3", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_76": { "label": "Route 104 - Berry Tree Flower Shop 4", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_77": { "label": "Route 114 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_78": { "label": "Route 114 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_79": { "label": "Route 123 - Berry Tree Berry Master 5", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_80": { "label": "Route 111 - Berry Tree 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_81": { "label": "Route 111 - Berry Tree 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_82": { "label": "Route 130 - Berry Tree on Mirage Island", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_83": { "label": "Route 119 - Berry Tree Above Waterfall 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_84": { "label": "Route 119 - Berry Tree Above Waterfall 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_85": { "label": "Route 119 - Berry Tree South 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_86": { "label": "Route 119 - Berry Tree South 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_87": { "label": "Route 123 - Berry Tree East 1", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "BERRY_TREE_88": { "label": "Route 123 - Berry Tree East 2", - "tags": ["BerryTree"] + "tags": [], + "category": "BERRY_TREE" }, "POKEDEX_REWARD_001": { "label": "Pokedex - Bulbasaur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_002": { "label": "Pokedex - Ivysaur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_003": { "label": "Pokedex - Venusaur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_004": { "label": "Pokedex - Charmander", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_005": { "label": "Pokedex - Charmeleon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_006": { "label": "Pokedex - Charizard", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_007": { "label": "Pokedex - Squirtle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_008": { "label": "Pokedex - Wartortle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_009": { "label": "Pokedex - Blastoise", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_010": { "label": "Pokedex - Caterpie", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_011": { "label": "Pokedex - Metapod", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_012": { "label": "Pokedex - Butterfree", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_013": { "label": "Pokedex - Weedle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_014": { "label": "Pokedex - Kakuna", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_015": { "label": "Pokedex - Beedrill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_016": { "label": "Pokedex - Pidgey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_017": { "label": "Pokedex - Pidgeotto", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_018": { "label": "Pokedex - Pidgeot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_019": { "label": "Pokedex - Rattata", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_020": { "label": "Pokedex - Raticate", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_021": { "label": "Pokedex - Spearow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_022": { "label": "Pokedex - Fearow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_023": { "label": "Pokedex - Ekans", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_024": { "label": "Pokedex - Arbok", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_025": { "label": "Pokedex - Pikachu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_026": { "label": "Pokedex - Raichu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_027": { "label": "Pokedex - Sandshrew", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_028": { "label": "Pokedex - Sandslash", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_029": { "label": "Pokedex - Nidoran Female", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_030": { "label": "Pokedex - Nidorina", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_031": { "label": "Pokedex - Nidoqueen", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_032": { "label": "Pokedex - Nidoran Male", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_033": { "label": "Pokedex - Nidorino", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_034": { "label": "Pokedex - Nidoking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_035": { "label": "Pokedex - Clefairy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_036": { "label": "Pokedex - Clefable", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_037": { "label": "Pokedex - Vulpix", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_038": { "label": "Pokedex - Ninetales", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_039": { "label": "Pokedex - Jigglypuff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_040": { "label": "Pokedex - Wigglytuff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_041": { "label": "Pokedex - Zubat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_042": { "label": "Pokedex - Golbat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_043": { "label": "Pokedex - Oddish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_044": { "label": "Pokedex - Gloom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_045": { "label": "Pokedex - Vileplume", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_046": { "label": "Pokedex - Paras", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_047": { "label": "Pokedex - Parasect", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_048": { "label": "Pokedex - Venonat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_049": { "label": "Pokedex - Venomoth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_050": { "label": "Pokedex - Diglett", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_051": { "label": "Pokedex - Dugtrio", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_052": { "label": "Pokedex - Meowth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_053": { "label": "Pokedex - Persian", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_054": { "label": "Pokedex - Psyduck", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_055": { "label": "Pokedex - Golduck", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_056": { "label": "Pokedex - Mankey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_057": { "label": "Pokedex - Primeape", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_058": { "label": "Pokedex - Growlithe", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_059": { "label": "Pokedex - Arcanine", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_060": { "label": "Pokedex - Poliwag", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_061": { "label": "Pokedex - Poliwhirl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_062": { "label": "Pokedex - Poliwrath", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_063": { "label": "Pokedex - Abra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_064": { "label": "Pokedex - Kadabra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_065": { "label": "Pokedex - Alakazam", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_066": { "label": "Pokedex - Machop", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_067": { "label": "Pokedex - Machoke", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_068": { "label": "Pokedex - Machamp", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_069": { "label": "Pokedex - Bellsprout", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_070": { "label": "Pokedex - Weepinbell", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_071": { "label": "Pokedex - Victreebel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_072": { "label": "Pokedex - Tentacool", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_073": { "label": "Pokedex - Tentacruel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_074": { "label": "Pokedex - Geodude", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_075": { "label": "Pokedex - Graveler", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_076": { "label": "Pokedex - Golem", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_077": { "label": "Pokedex - Ponyta", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_078": { "label": "Pokedex - Rapidash", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_079": { "label": "Pokedex - Slowpoke", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_080": { "label": "Pokedex - Slowbro", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_081": { "label": "Pokedex - Magnemite", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_082": { "label": "Pokedex - Magneton", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_083": { "label": "Pokedex - Farfetch'd", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_084": { "label": "Pokedex - Doduo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_085": { "label": "Pokedex - Dodrio", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_086": { "label": "Pokedex - Seel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_087": { "label": "Pokedex - Dewgong", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_088": { "label": "Pokedex - Grimer", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_089": { "label": "Pokedex - Muk", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_090": { "label": "Pokedex - Shellder", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_091": { "label": "Pokedex - Cloyster", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_092": { "label": "Pokedex - Gastly", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_093": { "label": "Pokedex - Haunter", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_094": { "label": "Pokedex - Gengar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_095": { "label": "Pokedex - Onix", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_096": { "label": "Pokedex - Drowzee", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_097": { "label": "Pokedex - Hypno", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_098": { "label": "Pokedex - Krabby", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_099": { "label": "Pokedex - Kingler", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_100": { "label": "Pokedex - Voltorb", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_101": { "label": "Pokedex - Electrode", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_102": { "label": "Pokedex - Exeggcute", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_103": { "label": "Pokedex - Exeggutor", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_104": { "label": "Pokedex - Cubone", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_105": { "label": "Pokedex - Marowak", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_106": { "label": "Pokedex - Hitmonlee", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_107": { "label": "Pokedex - Hitmonchan", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_108": { "label": "Pokedex - Lickitung", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_109": { "label": "Pokedex - Koffing", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_110": { "label": "Pokedex - Weezing", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_111": { "label": "Pokedex - Rhyhorn", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_112": { "label": "Pokedex - Rhydon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_113": { "label": "Pokedex - Chansey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_114": { "label": "Pokedex - Tangela", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_115": { "label": "Pokedex - Kangaskhan", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_116": { "label": "Pokedex - Horsea", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_117": { "label": "Pokedex - Seadra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_118": { "label": "Pokedex - Goldeen", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_119": { "label": "Pokedex - Seaking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_120": { "label": "Pokedex - Staryu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_121": { "label": "Pokedex - Starmie", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_122": { "label": "Pokedex - Mr. Mime", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_123": { "label": "Pokedex - Scyther", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_124": { "label": "Pokedex - Jynx", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_125": { "label": "Pokedex - Electabuzz", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_126": { "label": "Pokedex - Magmar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_127": { "label": "Pokedex - Pinsir", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_128": { "label": "Pokedex - Tauros", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_129": { "label": "Pokedex - Magikarp", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_130": { "label": "Pokedex - Gyarados", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_131": { "label": "Pokedex - Lapras", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_132": { "label": "Pokedex - Ditto", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_133": { "label": "Pokedex - Eevee", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_134": { "label": "Pokedex - Vaporeon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_135": { "label": "Pokedex - Jolteon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_136": { "label": "Pokedex - Flareon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_137": { "label": "Pokedex - Porygon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_138": { "label": "Pokedex - Omanyte", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_139": { "label": "Pokedex - Omastar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_140": { "label": "Pokedex - Kabuto", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_141": { "label": "Pokedex - Kabutops", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_142": { "label": "Pokedex - Aerodactyl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_143": { "label": "Pokedex - Snorlax", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_144": { "label": "Pokedex - Articuno", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_145": { "label": "Pokedex - Zapdos", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_146": { "label": "Pokedex - Moltres", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_147": { "label": "Pokedex - Dratini", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_148": { "label": "Pokedex - Dragonair", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_149": { "label": "Pokedex - Dragonite", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_150": { "label": "Pokedex - Mewtwo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_151": { "label": "Pokedex - Mew", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_152": { "label": "Pokedex - Chikorita", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_153": { "label": "Pokedex - Bayleef", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_154": { "label": "Pokedex - Meganium", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_155": { "label": "Pokedex - Cyndaquil", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_156": { "label": "Pokedex - Quilava", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_157": { "label": "Pokedex - Typhlosion", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_158": { "label": "Pokedex - Totodile", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_159": { "label": "Pokedex - Croconaw", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_160": { "label": "Pokedex - Feraligatr", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_161": { "label": "Pokedex - Sentret", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_162": { "label": "Pokedex - Furret", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_163": { "label": "Pokedex - Hoothoot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_164": { "label": "Pokedex - Noctowl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_165": { "label": "Pokedex - Ledyba", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_166": { "label": "Pokedex - Ledian", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_167": { "label": "Pokedex - Spinarak", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_168": { "label": "Pokedex - Ariados", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_169": { "label": "Pokedex - Crobat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_170": { "label": "Pokedex - Chinchou", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_171": { "label": "Pokedex - Lanturn", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_172": { "label": "Pokedex - Pichu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_173": { "label": "Pokedex - Cleffa", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_174": { "label": "Pokedex - Igglybuff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_175": { "label": "Pokedex - Togepi", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_176": { "label": "Pokedex - Togetic", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_177": { "label": "Pokedex - Natu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_178": { "label": "Pokedex - Xatu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_179": { "label": "Pokedex - Mareep", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_180": { "label": "Pokedex - Flaaffy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_181": { "label": "Pokedex - Ampharos", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_182": { "label": "Pokedex - Bellossom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_183": { "label": "Pokedex - Marill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_184": { "label": "Pokedex - Azumarill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_185": { "label": "Pokedex - Sudowoodo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_186": { "label": "Pokedex - Politoed", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_187": { "label": "Pokedex - Hoppip", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_188": { "label": "Pokedex - Skiploom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_189": { "label": "Pokedex - Jumpluff", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_190": { "label": "Pokedex - Aipom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_191": { "label": "Pokedex - Sunkern", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_192": { "label": "Pokedex - Sunflora", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_193": { "label": "Pokedex - Yanma", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_194": { "label": "Pokedex - Wooper", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_195": { "label": "Pokedex - Quagsire", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_196": { "label": "Pokedex - Espeon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_197": { "label": "Pokedex - Umbreon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_198": { "label": "Pokedex - Murkrow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_199": { "label": "Pokedex - Slowking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_200": { "label": "Pokedex - Misdreavus", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_201": { "label": "Pokedex - Unown", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_202": { "label": "Pokedex - Wobbuffet", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_203": { "label": "Pokedex - Girafarig", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_204": { "label": "Pokedex - Pineco", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_205": { "label": "Pokedex - Forretress", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_206": { "label": "Pokedex - Dunsparce", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_207": { "label": "Pokedex - Gligar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_208": { "label": "Pokedex - Steelix", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_209": { "label": "Pokedex - Snubbull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_210": { "label": "Pokedex - Granbull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_211": { "label": "Pokedex - Qwilfish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_212": { "label": "Pokedex - Scizor", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_213": { "label": "Pokedex - Shuckle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_214": { "label": "Pokedex - Heracross", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_215": { "label": "Pokedex - Sneasel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_216": { "label": "Pokedex - Teddiursa", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_217": { "label": "Pokedex - Ursaring", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_218": { "label": "Pokedex - Slugma", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_219": { "label": "Pokedex - Magcargo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_220": { "label": "Pokedex - Swinub", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_221": { "label": "Pokedex - Piloswine", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_222": { "label": "Pokedex - Corsola", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_223": { "label": "Pokedex - Remoraid", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_224": { "label": "Pokedex - Octillery", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_225": { "label": "Pokedex - Delibird", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_226": { "label": "Pokedex - Mantine", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_227": { "label": "Pokedex - Skarmory", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_228": { "label": "Pokedex - Houndour", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_229": { "label": "Pokedex - Houndoom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_230": { "label": "Pokedex - Kingdra", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_231": { "label": "Pokedex - Phanpy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_232": { "label": "Pokedex - Donphan", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_233": { "label": "Pokedex - Porygon2", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_234": { "label": "Pokedex - Stantler", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_235": { "label": "Pokedex - Smeargle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_236": { "label": "Pokedex - Tyrogue", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_237": { "label": "Pokedex - Hitmontop", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_238": { "label": "Pokedex - Smoochum", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_239": { "label": "Pokedex - Elekid", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_240": { "label": "Pokedex - Magby", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_241": { "label": "Pokedex - Miltank", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_242": { "label": "Pokedex - Blissey", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_243": { "label": "Pokedex - Raikou", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_244": { "label": "Pokedex - Entei", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_245": { "label": "Pokedex - Suicune", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_246": { "label": "Pokedex - Larvitar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_247": { "label": "Pokedex - Pupitar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_248": { "label": "Pokedex - Tyranitar", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_249": { "label": "Pokedex - Lugia", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_250": { "label": "Pokedex - Ho-Oh", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_251": { "label": "Pokedex - Celebi", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_252": { "label": "Pokedex - Treecko", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_253": { "label": "Pokedex - Grovyle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_254": { "label": "Pokedex - Sceptile", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_255": { "label": "Pokedex - Torchic", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_256": { "label": "Pokedex - Combusken", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_257": { "label": "Pokedex - Blaziken", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_258": { "label": "Pokedex - Mudkip", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_259": { "label": "Pokedex - Marshtomp", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_260": { "label": "Pokedex - Swampert", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_261": { "label": "Pokedex - Poochyena", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_262": { "label": "Pokedex - Mightyena", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_263": { "label": "Pokedex - Zigzagoon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_264": { "label": "Pokedex - Linoone", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_265": { "label": "Pokedex - Wurmple", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_266": { "label": "Pokedex - Silcoon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_267": { "label": "Pokedex - Beautifly", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_268": { "label": "Pokedex - Cascoon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_269": { "label": "Pokedex - Dustox", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_270": { "label": "Pokedex - Lotad", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_271": { "label": "Pokedex - Lombre", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_272": { "label": "Pokedex - Ludicolo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_273": { "label": "Pokedex - Seedot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_274": { "label": "Pokedex - Nuzleaf", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_275": { "label": "Pokedex - Shiftry", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_276": { "label": "Pokedex - Taillow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_277": { "label": "Pokedex - Swellow", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_278": { "label": "Pokedex - Wingull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_279": { "label": "Pokedex - Pelipper", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_280": { "label": "Pokedex - Ralts", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_281": { "label": "Pokedex - Kirlia", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_282": { "label": "Pokedex - Gardevoir", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_283": { "label": "Pokedex - Surskit", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_284": { "label": "Pokedex - Masquerain", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_285": { "label": "Pokedex - Shroomish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_286": { "label": "Pokedex - Breloom", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_287": { "label": "Pokedex - Slakoth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_288": { "label": "Pokedex - Vigoroth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_289": { "label": "Pokedex - Slaking", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_290": { "label": "Pokedex - Nincada", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_291": { "label": "Pokedex - Ninjask", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_292": { "label": "Pokedex - Shedinja", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_293": { "label": "Pokedex - Whismur", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_294": { "label": "Pokedex - Loudred", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_295": { "label": "Pokedex - Exploud", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_296": { "label": "Pokedex - Makuhita", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_297": { "label": "Pokedex - Hariyama", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_298": { "label": "Pokedex - Azurill", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_299": { "label": "Pokedex - Nosepass", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_300": { "label": "Pokedex - Skitty", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_301": { "label": "Pokedex - Delcatty", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_302": { "label": "Pokedex - Sableye", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_303": { "label": "Pokedex - Mawile", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_304": { "label": "Pokedex - Aron", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_305": { "label": "Pokedex - Lairon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_306": { "label": "Pokedex - Aggron", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_307": { "label": "Pokedex - Meditite", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_308": { "label": "Pokedex - Medicham", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_309": { "label": "Pokedex - Electrike", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_310": { "label": "Pokedex - Manectric", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_311": { "label": "Pokedex - Plusle", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_312": { "label": "Pokedex - Minun", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_313": { "label": "Pokedex - Volbeat", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_314": { "label": "Pokedex - Illumise", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_315": { "label": "Pokedex - Roselia", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_316": { "label": "Pokedex - Gulpin", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_317": { "label": "Pokedex - Swalot", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_318": { "label": "Pokedex - Carvanha", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_319": { "label": "Pokedex - Sharpedo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_320": { "label": "Pokedex - Wailmer", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_321": { "label": "Pokedex - Wailord", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_322": { "label": "Pokedex - Numel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_323": { "label": "Pokedex - Camerupt", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_324": { "label": "Pokedex - Torkoal", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_325": { "label": "Pokedex - Spoink", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_326": { "label": "Pokedex - Grumpig", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_327": { "label": "Pokedex - Spinda", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_328": { "label": "Pokedex - Trapinch", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_329": { "label": "Pokedex - Vibrava", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_330": { "label": "Pokedex - Flygon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_331": { "label": "Pokedex - Cacnea", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_332": { "label": "Pokedex - Cacturne", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_333": { "label": "Pokedex - Swablu", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_334": { "label": "Pokedex - Altaria", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_335": { "label": "Pokedex - Zangoose", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_336": { "label": "Pokedex - Seviper", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_337": { "label": "Pokedex - Lunatone", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_338": { "label": "Pokedex - Solrock", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_339": { "label": "Pokedex - Barboach", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_340": { "label": "Pokedex - Whiscash", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_341": { "label": "Pokedex - Corphish", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_342": { "label": "Pokedex - Crawdaunt", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_343": { "label": "Pokedex - Baltoy", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_344": { "label": "Pokedex - Claydol", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_345": { "label": "Pokedex - Lileep", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_346": { "label": "Pokedex - Cradily", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_347": { "label": "Pokedex - Anorith", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_348": { "label": "Pokedex - Armaldo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_349": { "label": "Pokedex - Feebas", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_350": { "label": "Pokedex - Milotic", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_351": { "label": "Pokedex - Castform", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_352": { "label": "Pokedex - Kecleon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_353": { "label": "Pokedex - Shuppet", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_354": { "label": "Pokedex - Banette", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_355": { "label": "Pokedex - Duskull", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_356": { "label": "Pokedex - Dusclops", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_357": { "label": "Pokedex - Tropius", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_358": { "label": "Pokedex - Chimecho", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_359": { "label": "Pokedex - Absol", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_360": { "label": "Pokedex - Wynaut", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_361": { "label": "Pokedex - Snorunt", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_362": { "label": "Pokedex - Glalie", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_363": { "label": "Pokedex - Spheal", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_364": { "label": "Pokedex - Sealeo", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_365": { "label": "Pokedex - Walrein", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_366": { "label": "Pokedex - Clamperl", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_367": { "label": "Pokedex - Huntail", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_368": { "label": "Pokedex - Gorebyss", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_369": { "label": "Pokedex - Relicanth", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_370": { "label": "Pokedex - Luvdisc", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_371": { "label": "Pokedex - Bagon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_372": { "label": "Pokedex - Shelgon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_373": { "label": "Pokedex - Salamence", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_374": { "label": "Pokedex - Beldum", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_375": { "label": "Pokedex - Metang", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_376": { "label": "Pokedex - Metagross", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_377": { "label": "Pokedex - Regirock", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_378": { "label": "Pokedex - Regice", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_379": { "label": "Pokedex - Registeel", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_380": { "label": "Pokedex - Latias", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_381": { "label": "Pokedex - Latios", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_382": { "label": "Pokedex - Kyogre", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_383": { "label": "Pokedex - Groudon", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_384": { "label": "Pokedex - Rayquaza", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_385": { "label": "Pokedex - Jirachi", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "POKEDEX_REWARD_386": { "label": "Pokedex - Deoxys", - "tags": ["Pokedex"] + "tags": [], + "category": "POKEDEX" }, "TRAINER_AARON_REWARD": { "label": "Route 134 - Dragon Tamer Aaron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ABIGAIL_1_REWARD": { "label": "Route 110 - Triathlete Abigail", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AIDAN_REWARD": { "label": "Route 127 - Bird Keeper Aidan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AISHA_REWARD": { "label": "Route 117 - Battle Girl Aisha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALBERTO_REWARD": { "label": "Route 123 - Bird Keeper Alberto", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALBERT_REWARD": { "label": "Victory Road 1F - Cooltrainer Albert", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALEXA_REWARD": { "label": "Route 128 - Cooltrainer Alexa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALEXIA_REWARD": { "label": "Petalburg Gym - Cooltrainer Alexia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALEX_REWARD": { "label": "Route 134 - Bird Keeper Alex", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALICE_REWARD": { "label": "Route 109 - Swimmer Alice", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALIX_REWARD": { "label": "Route 115 - Psychic Alix", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALLEN_REWARD": { "label": "Route 102 - Youngster Allen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALLISON_REWARD": { "label": "Route 129 - Triathlete Allison", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ALYSSA_REWARD": { "label": "Route 110 - Triathlete Alyssa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AMY_AND_LIV_1_REWARD": { "label": "Route 103 - Twins Amy and Liv", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANNA_AND_MEG_1_REWARD": { "label": "Route 117 - Sr. and Jr. Anna and Meg", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANDREA_REWARD": { "label": "Sootopolis Gym - Lass Andrea", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANDRES_1_REWARD": { "label": "Route 105 - Ruin Maniac Andres", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANDREW_REWARD": { "label": "Route 103 - Fisherman Andrew", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANGELICA_REWARD": { "label": "Route 120 - Parasol Lady Angelica", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANGELINA_REWARD": { "label": "Route 114 - Picnicker Angelina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANGELO_REWARD": { "label": "Mauville Gym - Bug Maniac Angelo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANNIKA_REWARD": { "label": "Sootopolis Gym - Pokefan Annika", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ANTHONY_REWARD": { "label": "Route 110 - Triathlete Anthony", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ARCHIE_REWARD": { "label": "Seafloor Cavern Room 9 - Aqua Leader Archie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ASHLEY_REWARD": { "label": "Fortree Gym - Picnicker Ashley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ATHENA_REWARD": { "label": "Route 127 - Cooltrainer Athena", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ATSUSHI_REWARD": { "label": "Mt Pyre 5F - Black Belt Atsushi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AURON_REWARD": { "label": "Route 125 - Expert Auron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AUSTINA_REWARD": { "label": "Route 109 - Tuber Austina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AUTUMN_REWARD": { "label": "Jagged Pass - Picnicker Autumn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_AXLE_REWARD": { "label": "Lavaridge Gym - Kindler Axle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BARNY_REWARD": { "label": "Route 118 - Fisherman Barny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BARRY_REWARD": { "label": "Route 126 - Swimmer Barry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BEAU_REWARD": { "label": "Route 111 - Camper Beau", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BECKY_REWARD": { "label": "Route 111 - Picnicker Becky", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BECK_REWARD": { "label": "Route 133 - Bird Keeper Beck", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BENJAMIN_1_REWARD": { "label": "Route 110 - Triathlete Benjamin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BEN_REWARD": { "label": "Mauville Gym - Youngster Ben", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BERKE_REWARD": { "label": "Petalburg Gym - Cooltrainer Berke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BERNIE_1_REWARD": { "label": "Route 114 - Kindler Bernie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BETHANY_REWARD": { "label": "Sootopolis Gym - Pokefan Bethany", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BETH_REWARD": { "label": "Route 107 - Swimmer Beth", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BEVERLY_REWARD": { "label": "Route 105 - Swimmer Beverly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BIANCA_REWARD": { "label": "Route 111 - Picnicker Bianca", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BILLY_REWARD": { "label": "Route 104 - Youngster Billy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BLAKE_REWARD": { "label": "Mossdeep Gym - Psychic Blake", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRANDEN_REWARD": { "label": "Route 111 - Camper Branden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRANDI_REWARD": { "label": "Route 117 - Psychic Brandi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRAWLY_1_REWARD": { "label": "Dewford Gym - Leader Brawly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRAXTON_REWARD": { "label": "Route 123 - Cooltrainer Braxton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_LILYCOVE_MUDKIP_REWARD": { "label": "Lilycove City - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_ROUTE_103_MUDKIP_REWARD": { "label": "Route 103 - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_ROUTE_110_MUDKIP_REWARD": { "label": "Route 110 - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_ROUTE_119_MUDKIP_REWARD": { "label": "Route 119 - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDAN_RUSTBORO_MUDKIP_REWARD": { "label": "Rustboro City - Rival Brendan/May", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDA_REWARD": { "label": "Route 126 - Swimmer Brenda", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENDEN_REWARD": { "label": "Dewford Gym - Sailor Brenden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRENT_REWARD": { "label": "Route 119 - Bug Maniac Brent", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRIANNA_REWARD": { "label": "Sootopolis Gym - Lady Brianna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRICE_REWARD": { "label": "Route 112 - Hiker Brice", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRIDGET_REWARD": { "label": "Sootopolis Gym - Beauty Bridget", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BROOKE_1_REWARD": { "label": "Route 111 - Cooltrainer Brooke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRYANT_REWARD": { "label": "Route 112 - Kindler Bryant", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_BRYAN_REWARD": { "label": "Route 111 - Ruin Maniac Bryan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CALE_REWARD": { "label": "Route 121 - Bug Maniac Cale", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CALLIE_REWARD": { "label": "Route 120 - Battle Girl Callie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CALVIN_1_REWARD": { "label": "Route 102 - Youngster Calvin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAMDEN_REWARD": { "label": "Route 127 - Triathlete Camden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAMERON_1_REWARD": { "label": "Route 123 - Psychic Cameron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAMRON_REWARD": { "label": "Route 107 - Triathlete Camron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CARLEE_REWARD": { "label": "Route 128 - Swimmer Carlee", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAROLINA_REWARD": { "label": "Route 108 - Cooltrainer Carolina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAROLINE_REWARD": { "label": "Victory Road B2F - Cooltrainer Caroline", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CAROL_REWARD": { "label": "Route 112 - Picnicker Carol", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CARTER_REWARD": { "label": "Route 109 - Fisherman Carter", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CATHERINE_1_REWARD": { "label": "Route 119 - Pokemon Ranger Catherine", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CEDRIC_REWARD": { "label": "Mt Pyre 6F - Psychic Cedric", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CELIA_REWARD": { "label": "Route 111 - Picnicker Celia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CELINA_REWARD": { "label": "Route 111 - Aroma Lady Celina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHAD_REWARD": { "label": "Route 124 - Swimmer Chad", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHANDLER_REWARD": { "label": "Route 109 - Tuber Chandler", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHARLIE_REWARD": { "label": "Abandoned Ship 1F - Tuber Charlie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHARLOTTE_REWARD": { "label": "Route 114 - Picnicker Charlotte", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHASE_REWARD": { "label": "Route 129 - Triathlete Chase", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHESTER_REWARD": { "label": "Route 118 - Bird Keeper Chester", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHIP_REWARD": { "label": "Route 120 - Ruin Maniac Chip", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CHRIS_REWARD": { "label": "Route 119 - Fisherman Chris", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CINDY_1_REWARD": { "label": "Route 104 - Lady Cindy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLARENCE_REWARD": { "label": "Route 129 - Swimmer Clarence", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLARISSA_REWARD": { "label": "Route 120 - Parasol Lady Clarissa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLARK_REWARD": { "label": "Route 116 - Hiker Clark", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLAUDE_REWARD": { "label": "Route 114 - Fisherman Claude", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CLIFFORD_REWARD": { "label": "Mossdeep Gym - Gentleman Clifford", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COBY_REWARD": { "label": "Route 113 - Bird Keeper Coby", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COLE_REWARD": { "label": "Lavaridge Gym - Kindler Cole", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COLIN_REWARD": { "label": "Route 120 - Bird Keeper Colin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_COLTON_REWARD": { "label": "SS Tidal - Pokefan Colton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CONNIE_REWARD": { "label": "Sootopolis Gym - Beauty Connie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CONOR_REWARD": { "label": "Route 133 - Expert Conor", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CORY_1_REWARD": { "label": "Route 108 - Sailor Cory", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CRISSY_REWARD": { "label": "Sootopolis Gym - Lass Crissy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CRISTIAN_REWARD": { "label": "Dewford Gym - Black Belt Cristian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CRISTIN_1_REWARD": { "label": "Route 121 - Cooltrainer Cristin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_CYNDY_1_REWARD": { "label": "Route 115 - Battle Girl Cyndy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAISUKE_REWARD": { "label": "Route 111 - Black Belt Daisuke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAISY_REWARD": { "label": "Route 103 - Aroma Lady Daisy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DALE_REWARD": { "label": "Route 110 - Fisherman Dale", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DALTON_1_REWARD": { "label": "Route 118 - Guitarist Dalton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DANA_REWARD": { "label": "Route 132 - Swimmer Dana", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DANIELLE_REWARD": { "label": "Lavaridge Gym - Battle Girl Danielle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAPHNE_REWARD": { "label": "Sootopolis Gym - Lady Daphne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARCY_REWARD": { "label": "Route 132 - Cooltrainer Darcy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARIAN_REWARD": { "label": "Route 104 - Fisherman Darian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARIUS_REWARD": { "label": "Fortree Gym - Bird Keeper Darius", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DARRIN_REWARD": { "label": "Route 107 - Swimmer Darrin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAVID_REWARD": { "label": "Route 109 - Swimmer David", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAVIS_REWARD": { "label": "Route 123 - Bug Catcher Davis", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAWSON_REWARD": { "label": "Route 116 - Rich Boy Dawson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DAYTON_REWARD": { "label": "Route 119 - Kindler Dayton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEANDRE_REWARD": { "label": "Route 118 - Youngster Deandre", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEAN_REWARD": { "label": "Route 126 - Swimmer Dean", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEBRA_REWARD": { "label": "Route 133 - Swimmer Debra", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DECLAN_REWARD": { "label": "Route 124 - Swimmer Declan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEMETRIUS_REWARD": { "label": "Abandoned Ship 1F - Youngster Demetrius", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DENISE_REWARD": { "label": "Route 107 - Swimmer Denise", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEREK_REWARD": { "label": "Route 117 - Bug Maniac Derek", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEVAN_REWARD": { "label": "Route 116 - Hiker Devan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DIANA_1_REWARD": { "label": "Jagged Pass - Picnicker Diana", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DIANNE_REWARD": { "label": "Victory Road B2F - Cooltrainer Dianne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DILLON_REWARD": { "label": "Route 113 - Youngster Dillon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DOMINIK_REWARD": { "label": "Route 105 - Swimmer Dominik", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DONALD_REWARD": { "label": "Route 119 - Bug Maniac Donald", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DONNY_REWARD": { "label": "Route 127 - Triathlete Donny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DOUGLAS_REWARD": { "label": "Route 106 - Swimmer Douglas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DOUG_REWARD": { "label": "Route 119 - Bug Catcher Doug", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DRAKE_REWARD": { "label": "Ever Grande City - Elite Four Drake", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DREW_REWARD": { "label": "Route 111 - Camper Drew", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DUNCAN_REWARD": { "label": "Abandoned Ship B1F - Sailor Duncan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DUSTY_1_REWARD": { "label": "Route 111 - Ruin Maniac Dusty", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DWAYNE_REWARD": { "label": "Route 109 - Sailor Dwayne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DYLAN_1_REWARD": { "label": "Route 117 - Triathlete Dylan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_DEZ_AND_LUKE_REWARD": { "label": "Mt Pyre 2F - Young Couple Dez and Luke", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDGAR_REWARD": { "label": "Victory Road 1F - Cooltrainer Edgar", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDMOND_REWARD": { "label": "Route 109 - Sailor Edmond", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDWARDO_REWARD": { "label": "Fortree Gym - Bird Keeper Edwardo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDWARD_REWARD": { "label": "Route 110 - Psychic Edward", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_EDWIN_1_REWARD": { "label": "Route 110 - Collector Edwin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ED_REWARD": { "label": "Route 123 - Collector Ed", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ELIJAH_REWARD": { "label": "Route 109 - Bird Keeper Elijah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ELI_REWARD": { "label": "Lavaridge Gym - Hiker Eli", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ELLIOT_1_REWARD": { "label": "Route 106 - Fisherman Elliot", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ERIC_REWARD": { "label": "Jagged Pass - Hiker Eric", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ERNEST_1_REWARD": { "label": "Route 125 - Sailor Ernest", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ETHAN_1_REWARD": { "label": "Jagged Pass - Camper Ethan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FABIAN_REWARD": { "label": "Route 119 - Guitarist Fabian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FELIX_REWARD": { "label": "Victory Road B2F - Cooltrainer Felix", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FERNANDO_1_REWARD": { "label": "Route 123 - Guitarist Fernando", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FLANNERY_1_REWARD": { "label": "Lavaridge Gym - Leader Flannery", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FLINT_REWARD": { "label": "Fortree Gym - Camper Flint", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FOSTER_REWARD": { "label": "Route 105 - Ruin Maniac Foster", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FRANKLIN_REWARD": { "label": "Route 133 - Swimmer Franklin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_FREDRICK_REWARD": { "label": "Route 123 - Expert Fredrick", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GABRIELLE_1_REWARD": { "label": "Mt Pyre 3F - Pokemon Breeder Gabrielle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GARRET_REWARD": { "label": "SS Tidal - Rich Boy Garret", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GARRISON_REWARD": { "label": "Abandoned Ship 1F - Ruin Maniac Garrison", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GEORGE_REWARD": { "label": "Petalburg Gym - Cooltrainer George", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GERALD_REWARD": { "label": "Lavaridge Gym - Cooltrainer Gerald", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GILBERT_REWARD": { "label": "Route 132 - Swimmer Gilbert", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GINA_AND_MIA_1_REWARD": { "label": "Route 104 - Twins Gina and Mia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GLACIA_REWARD": { "label": "Ever Grande City - Elite Four Glacia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRACE_REWARD": { "label": "Route 124 - Swimmer Grace", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GREG_REWARD": { "label": "Route 119 - Bug Catcher Greg", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_1_REWARD": { "label": "Aqua Hideout 1F - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_2_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_3_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_4_REWARD": { "label": "Aqua Hideout B2F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_5_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_6_REWARD": { "label": "Aqua Hideout B2F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_7_REWARD": { "label": "Aqua Hideout B1F - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_AQUA_HIDEOUT_8_REWARD": { "label": "Aqua Hideout B2F - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_10_REWARD": { "label": "Magma Hideout 3F - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_11_REWARD": { "label": "Magma Hideout 4F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_12_REWARD": { "label": "Magma Hideout 4F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_13_REWARD": { "label": "Magma Hideout 4F - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_14_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_15_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 5", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_16_REWARD": { "label": "Magma Hideout 3F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_1_REWARD": { "label": "Magma Hideout 1F - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_2_REWARD": { "label": "Magma Hideout 1F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_3_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_4_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_5_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_6_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 6", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_7_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 7", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_8_REWARD": { "label": "Magma Hideout 2F - Team Magma Grunt 8", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MAGMA_HIDEOUT_9_REWARD": { "label": "Magma Hideout 3F - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_CHIMNEY_1_REWARD": { "label": "Mt Chimney - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_CHIMNEY_2_REWARD": { "label": "Mt Chimney - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_1_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_2_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_3_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MT_PYRE_4_REWARD": { "label": "Mt Pyre Summit - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MUSEUM_1_REWARD": { "label": "Oceanic Museum - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_MUSEUM_2_REWARD": { "label": "Oceanic Museum - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_PETALBURG_WOODS_REWARD": { "label": "Petalburg Woods - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_RUSTURF_TUNNEL_REWARD": { "label": "Rusturf Tunnel - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_1_REWARD": { "label": "Seafloor Cavern Room 1 - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_2_REWARD": { "label": "Seafloor Cavern Room 1 - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_3_REWARD": { "label": "Seafloor Cavern Room 4 - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_4_REWARD": { "label": "Seafloor Cavern Room 4 - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SEAFLOOR_CAVERN_5_REWARD": { "label": "Seafloor Cavern Room 3 - Team Aqua Grunt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_1_REWARD": { "label": "Space Center - Team Magma Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_2_REWARD": { "label": "Space Center - Team Magma Grunt 4", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_3_REWARD": { "label": "Space Center - Team Magma Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_4_REWARD": { "label": "Space Center - Team Magma Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_5_REWARD": { "label": "Space Center - Team Magma Grunt 5", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_6_REWARD": { "label": "Space Center - Team Magma Grunt 6", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_SPACE_CENTER_7_REWARD": { "label": "Space Center - Team Magma Grunt 7", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_1_REWARD": { "label": "Weather Institute 1F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_2_REWARD": { "label": "Weather Institute 2F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_3_REWARD": { "label": "Weather Institute 2F - Team Aqua Grunt 3", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_4_REWARD": { "label": "Weather Institute 1F - Team Aqua Grunt 1", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GRUNT_WEATHER_INST_5_REWARD": { "label": "Weather Institute 2F - Team Aqua Grunt 2", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_GWEN_REWARD": { "label": "Route 109 - Tuber Gwen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HAILEY_REWARD": { "label": "Route 109 - Tuber Hailey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HALEY_1_REWARD": { "label": "Route 104 - Lass Haley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HALLE_REWARD": { "label": "Victory Road B1F - Cooltrainer Halle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HANNAH_REWARD": { "label": "Mossdeep Gym - Psychic Hannah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HARRISON_REWARD": { "label": "Route 128 - Swimmer Harrison", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HAYDEN_REWARD": { "label": "Route 111 - Kindler Hayden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HECTOR_REWARD": { "label": "Route 115 - Collector Hector", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HEIDI_REWARD": { "label": "Route 111 - Picnicker Heidi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HELENE_REWARD": { "label": "Route 115 - Battle Girl Helene", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HENRY_REWARD": { "label": "Route 127 - Fisherman Henry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HERMAN_REWARD": { "label": "Route 131 - Swimmer Herman", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HIDEO_REWARD": { "label": "Route 119 - Ninja Boy Hideo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HITOSHI_REWARD": { "label": "Route 134 - Black Belt Hitoshi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HOPE_REWARD": { "label": "Victory Road 1F - Cooltrainer Hope", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUDSON_REWARD": { "label": "Route 134 - Sailor Hudson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUEY_REWARD": { "label": "Route 109 - Sailor Huey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUGH_REWARD": { "label": "Route 119 - Bird Keeper Hugh", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_HUMBERTO_REWARD": { "label": "Fortree Gym - Bird Keeper Humberto", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_IMANI_REWARD": { "label": "Route 105 - Swimmer Imani", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_IRENE_REWARD": { "label": "Route 111 - Picnicker Irene", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISAAC_1_REWARD": { "label": "Route 117 - Pokemon Breeder Isaac", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISABELLA_REWARD": { "label": "Route 124 - Triathlete Isabella", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISABELLE_REWARD": { "label": "Route 103 - Swimmer Isabelle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISABEL_1_REWARD": { "label": "Route 110 - Pokefan Isabel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISAIAH_1_REWARD": { "label": "Route 128 - Triathlete Isaiah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ISOBEL_REWARD": { "label": "Route 126 - Triathlete Isobel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_IVAN_REWARD": { "label": "Route 104 - Fisherman Ivan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACE_REWARD": { "label": "Lavaridge Gym - Kindler Jace", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACKI_1_REWARD": { "label": "Route 123 - Psychic Jacki", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACKSON_1_REWARD": { "label": "Route 119 - Pokemon Ranger Jackson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACK_REWARD": { "label": "Route 134 - Swimmer Jack", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACLYN_REWARD": { "label": "Route 110 - Psychic Jaclyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JACOB_REWARD": { "label": "Route 110 - Triathlete Jacob", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAIDEN_REWARD": { "label": "Route 115 - Ninja Boy Jaiden", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAMES_1_REWARD": { "label": "Petalburg Woods - Bug Catcher James", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JANICE_REWARD": { "label": "Route 116 - Lass Janice", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JANI_REWARD": { "label": "Abandoned Ship 1F - Tuber Jani", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JARED_REWARD": { "label": "Fortree Gym - Bird Keeper Jared", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JASMINE_REWARD": { "label": "Route 110 - Triathlete Jasmine", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAYLEN_REWARD": { "label": "Route 113 - Youngster Jaylen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JAZMYN_REWARD": { "label": "Route 123 - Cooltrainer Jazmyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JEFFREY_1_REWARD": { "label": "Route 120 - Bug Maniac Jeffrey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JEFF_REWARD": { "label": "Lavaridge Gym - Kindler Jeff", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JENNA_REWARD": { "label": "Route 120 - Pokemon Ranger Jenna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JENNIFER_REWARD": { "label": "Route 120 - Cooltrainer Jennifer", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JENNY_1_REWARD": { "label": "Route 124 - Swimmer Jenny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JEROME_REWARD": { "label": "Route 108 - Swimmer Jerome", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JERRY_1_REWARD": { "label": "Route 116 - School Kid Jerry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JESSICA_1_REWARD": { "label": "Route 121 - Beauty Jessica", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOCELYN_REWARD": { "label": "Dewford Gym - Battle Girl Jocelyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JODY_REWARD": { "label": "Petalburg Gym - Cooltrainer Jody", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOEY_REWARD": { "label": "Route 116 - Youngster Joey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOHANNA_REWARD": { "label": "Route 109 - Beauty Johanna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOHN_AND_JAY_1_REWARD": { "label": "Meteor Falls 1F - Old Couple John and Jay", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOHNSON_REWARD": { "label": "Route 116 - Youngster Johnson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JONAH_REWARD": { "label": "Route 127 - Fisherman Jonah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JONAS_REWARD": { "label": "Route 123 - Ninja Boy Jonas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JONATHAN_REWARD": { "label": "Route 132 - Cooltrainer Jonathan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSEPH_REWARD": { "label": "Route 110 - Guitarist Joseph", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSE_REWARD": { "label": "Route 116 - Bug Catcher Jose", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSH_REWARD": { "label": "Rustboro Gym - Youngster Josh", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JOSUE_REWARD": { "label": "Route 105 - Bird Keeper Josue", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JUAN_1_REWARD": { "label": "Sootopolis Gym - Leader Juan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JULIE_REWARD": { "label": "Victory Road B2F - Cooltrainer Julie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_JULIO_REWARD": { "label": "Jagged Pass - Triathlete Julio", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAI_REWARD": { "label": "Route 114 - Fisherman Kai", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KALEB_REWARD": { "label": "Route 110 - Pokefan Kaleb", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KARA_REWARD": { "label": "Route 131 - Swimmer Kara", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAREN_1_REWARD": { "label": "Route 116 - School Kid Karen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATE_AND_JOY_REWARD": { "label": "Route 121 - Sr. and Jr. Kate and Joy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATELYNN_REWARD": { "label": "Victory Road 1F - Cooltrainer Katelynn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATELYN_1_REWARD": { "label": "Route 128 - Triathlete Katelyn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATHLEEN_REWARD": { "label": "Mossdeep Gym - Hex Maniac Kathleen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KATIE_REWARD": { "label": "Route 130 - Swimmer Katie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAYLA_REWARD": { "label": "Mt Pyre 3F - Psychic Kayla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KAYLEY_REWARD": { "label": "Route 123 - Parasol Lady Kayley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KEEGAN_REWARD": { "label": "Lavaridge Gym - Kindler Keegan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KEIGO_REWARD": { "label": "Route 120 - Ninja Boy Keigo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KELVIN_REWARD": { "label": "Route 134 - Sailor Kelvin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KENT_REWARD": { "label": "Route 119 - Bug Catcher Kent", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KEVIN_REWARD": { "label": "Route 131 - Swimmer Kevin", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIM_AND_IRIS_REWARD": { "label": "Route 125 - Sr. and Jr. Kim and Iris", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KINDRA_REWARD": { "label": "Route 123 - Hex Maniac Kindra", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIRA_AND_DAN_1_REWARD": { "label": "Abandoned Ship 1F - Young Couple Kira and Dan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIRK_REWARD": { "label": "Mauville Gym - Guitarist Kirk", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KIYO_REWARD": { "label": "Route 132 - Black Belt Kiyo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KOICHI_REWARD": { "label": "Route 115 - Black Belt Koichi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KOJI_1_REWARD": { "label": "Route 127 - Black Belt Koji", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KYLA_REWARD": { "label": "Route 106 - Swimmer Kyla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_KYRA_REWARD": { "label": "Route 115 - Triathlete Kyra", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAO_1_REWARD": { "label": "Route 113 - Ninja Boy Lao", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LARRY_REWARD": { "label": "Route 112 - Camper Larry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAURA_REWARD": { "label": "Dewford Gym - Battle Girl Laura", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAUREL_REWARD": { "label": "Route 134 - Swimmer Laurel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LAWRENCE_REWARD": { "label": "Route 113 - Camper Lawrence", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEA_AND_JED_REWARD": { "label": "SS Tidal - Young Couple Lea and Jed", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEAH_REWARD": { "label": "Mt Pyre 2F - Hex Maniac Leah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LENNY_REWARD": { "label": "Route 114 - Hiker Lenny", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEONARDO_REWARD": { "label": "Route 126 - Swimmer Leonardo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEONARD_REWARD": { "label": "SS Tidal - Sailor Leonard", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LEONEL_REWARD": { "label": "Route 120 - Cooltrainer Leonel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LILA_AND_ROY_1_REWARD": { "label": "Route 124 - Sis and Bro Lila and Roy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LILITH_REWARD": { "label": "Dewford Gym - Battle Girl Lilith", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LINDA_REWARD": { "label": "Route 133 - Swimmer Linda", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LISA_AND_RAY_REWARD": { "label": "Route 107 - Sis and Bro Lisa and Ray", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LOLA_1_REWARD": { "label": "Route 109 - Tuber Lola", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LORENZO_REWARD": { "label": "Route 120 - Pokemon Ranger Lorenzo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LUCAS_1_REWARD": { "label": "Route 114 - Hiker Lucas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LUIS_REWARD": { "label": "Route 105 - Swimmer Luis", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LUNG_REWARD": { "label": "Route 113 - Ninja Boy Lung", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LYDIA_1_REWARD": { "label": "Route 117 - Pokemon Breeder Lydia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_LYLE_REWARD": { "label": "Petalburg Woods - Bug Catcher Lyle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MACEY_REWARD": { "label": "Mossdeep Gym - Psychic Macey", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MADELINE_1_REWARD": { "label": "Route 113 - Parasol Lady Madeline", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAKAYLA_REWARD": { "label": "Route 132 - Expert Makayla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARCEL_REWARD": { "label": "Route 121 - Cooltrainer Marcel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARCOS_REWARD": { "label": "Route 103 - Guitarist Marcos", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARC_REWARD": { "label": "Rustboro Gym - Hiker Marc", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARIA_1_REWARD": { "label": "Route 117 - Triathlete Maria", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARK_REWARD": { "label": "Mt Pyre 2F - Pokemaniac Mark", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARLENE_REWARD": { "label": "Route 115 - Psychic Marlene", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARLEY_REWARD": { "label": "Route 134 - Cooltrainer Marley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MARY_REWARD": { "label": "Petalburg Gym - Cooltrainer Mary", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MATTHEW_REWARD": { "label": "Route 108 - Swimmer Matthew", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MATT_REWARD": { "label": "Aqua Hideout B2F - Aqua Admin Matt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAURA_REWARD": { "label": "Mossdeep Gym - Psychic Maura", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAXIE_MAGMA_HIDEOUT_REWARD": { "label": "Magma Hideout 4F - Magma Leader Maxie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MAXIE_MT_CHIMNEY_REWARD": { "label": "Mt Chimney - Magma Leader Maxie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MEL_AND_PAUL_REWARD": { "label": "Route 109 - Young Couple Mel and Paul", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MELINA_REWARD": { "label": "Route 117 - Triathlete Melina", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MELISSA_REWARD": { "label": "Mt Chimney - Beauty Melissa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MICAH_REWARD": { "label": "SS Tidal - Gentleman Micah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MICHELLE_REWARD": { "label": "Victory Road B1F - Cooltrainer Michelle", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MIGUEL_1_REWARD": { "label": "Route 103 - Pokefan Miguel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MIKE_2_REWARD": { "label": "Rusturf Tunnel - Hiker Mike", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MISSY_REWARD": { "label": "Route 108 - Swimmer Missy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MITCHELL_REWARD": { "label": "Victory Road B1F - Cooltrainer Mitchell", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MIU_AND_YUKI_REWARD": { "label": "Route 123 - Twins Miu and Yuki", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MOLLIE_REWARD": { "label": "Route 133 - Expert Mollie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_MYLES_REWARD": { "label": "Route 121 - Pokemon Breeder Myles", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NANCY_REWARD": { "label": "Route 114 - Picnicker Nancy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NAOMI_REWARD": { "label": "SS Tidal - Lady Naomi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NATE_REWARD": { "label": "Mossdeep Gym - Gentleman Nate", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NED_REWARD": { "label": "Route 106 - Fisherman Ned", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NICHOLAS_REWARD": { "label": "Mossdeep Gym - Psychic Nicholas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NICOLAS_1_REWARD": { "label": "Meteor Falls 1F - Dragon Tamer Nicolas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NIKKI_REWARD": { "label": "Route 126 - Swimmer Nikki", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NOB_1_REWARD": { "label": "Route 115 - Black Belt Nob", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NOLAN_REWARD": { "label": "Route 114 - Fisherman Nolan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NOLEN_REWARD": { "label": "Route 125 - Swimmer Nolen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_NORMAN_1_REWARD": { "label": "Petalburg Gym - Leader Norman", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_OLIVIA_REWARD": { "label": "Sootopolis Gym - Beauty Olivia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_OWEN_REWARD": { "label": "Victory Road B2F - Cooltrainer Owen", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PABLO_1_REWARD": { "label": "Route 126 - Triathlete Pablo", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PARKER_REWARD": { "label": "Petalburg Gym - Cooltrainer Parker", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PAT_REWARD": { "label": "Route 121 - Pokemon Breeder Pat", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PAXTON_REWARD": { "label": "Route 132 - Expert Paxton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PERRY_REWARD": { "label": "Route 118 - Bird Keeper Perry", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PETE_REWARD": { "label": "Route 103 - Swimmer Pete", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PHILLIP_REWARD": { "label": "SS Tidal - Sailor Phillip", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PHIL_REWARD": { "label": "Route 119 - Bird Keeper Phil", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PHOEBE_REWARD": { "label": "Ever Grande City - Elite Four Phoebe", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PRESLEY_REWARD": { "label": "Route 125 - Bird Keeper Presley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_PRESTON_REWARD": { "label": "Mossdeep Gym - Psychic Preston", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_QUINCY_REWARD": { "label": "Victory Road 1F - Cooltrainer Quincy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RACHEL_REWARD": { "label": "Route 119 - Parasol Lady Rachel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RANDALL_REWARD": { "label": "Petalburg Gym - Cooltrainer Randall", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_REED_REWARD": { "label": "Route 129 - Swimmer Reed", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RELI_AND_IAN_REWARD": { "label": "Route 131 - Sis and Bro Reli and Ian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_REYNA_REWARD": { "label": "Route 134 - Battle Girl Reyna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RHETT_REWARD": { "label": "Route 103 - Black Belt Rhett", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RICHARD_REWARD": { "label": "Route 131 - Swimmer Richard", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RICKY_1_REWARD": { "label": "Route 109 - Tuber Ricky", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RICK_REWARD": { "label": "Route 102 - Bug Catcher Rick", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RILEY_REWARD": { "label": "Route 120 - Ninja Boy Riley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROBERT_1_REWARD": { "label": "Route 120 - Bird Keeper Robert", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RODNEY_REWARD": { "label": "Route 130 - Swimmer Rodney", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROGER_REWARD": { "label": "Route 127 - Fisherman Roger", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROLAND_REWARD": { "label": "Route 124 - Swimmer Roland", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RONALD_REWARD": { "label": "Route 132 - Fisherman Ronald", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROSE_1_REWARD": { "label": "Route 118 - Aroma Lady Rose", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ROXANNE_1_REWARD": { "label": "Rustboro Gym - Leader Roxanne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_RUBEN_REWARD": { "label": "Route 128 - Cooltrainer Ruben", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SAMANTHA_REWARD": { "label": "Mossdeep Gym - Psychic Samantha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SAMUEL_REWARD": { "label": "Victory Road B1F - Cooltrainer Samuel", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SANTIAGO_REWARD": { "label": "Route 130 - Swimmer Santiago", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SARAH_REWARD": { "label": "Route 116 - Lady Sarah", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SAWYER_1_REWARD": { "label": "Mt Chimney - Hiker Sawyer", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHANE_REWARD": { "label": "Route 114 - Camper Shane", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHANNON_REWARD": { "label": "Victory Road B1F - Cooltrainer Shannon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHARON_REWARD": { "label": "Route 125 - Swimmer Sharon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHAWN_REWARD": { "label": "Mauville Gym - Guitarist Shawn", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHAYLA_REWARD": { "label": "Route 112 - Aroma Lady Shayla", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHEILA_REWARD": { "label": "Mt Chimney - Beauty Sheila", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHELBY_1_REWARD": { "label": "Mt Chimney - Expert Shelby", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHELLY_SEAFLOOR_CAVERN_REWARD": { "label": "Seafloor Cavern Room 3 - Aqua Admin Shelly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHELLY_WEATHER_INSTITUTE_REWARD": { "label": "Weather Institute 2F - Aqua Admin Shelly", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SHIRLEY_REWARD": { "label": "Mt Chimney - Beauty Shirley", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SIDNEY_REWARD": { "label": "Ever Grande City - Elite Four Sidney", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SIENNA_REWARD": { "label": "Route 126 - Swimmer Sienna", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SIMON_REWARD": { "label": "Route 109 - Tuber Simon", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SOPHIE_REWARD": { "label": "Route 113 - Picnicker Sophie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SPENCER_REWARD": { "label": "Route 124 - Swimmer Spencer", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_STAN_REWARD": { "label": "Route 125 - Swimmer Stan", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_STEVEN_REWARD": { "label": "Meteor Falls 1F - Rival Steven", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_STEVE_1_REWARD": { "label": "Route 114 - Pokemaniac Steve", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SUSIE_REWARD": { "label": "Route 131 - Swimmer Susie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_SYLVIA_REWARD": { "label": "Mossdeep Gym - Hex Maniac Sylvia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TABITHA_MAGMA_HIDEOUT_REWARD": { "label": "Magma Hideout 4F - Magma Admin Tabitha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TABITHA_MT_CHIMNEY_REWARD": { "label": "Mt Chimney - Magma Admin Tabitha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAKAO_REWARD": { "label": "Dewford Gym - Black Belt Takao", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAKASHI_REWARD": { "label": "Route 119 - Ninja Boy Takashi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TALIA_REWARD": { "label": "Route 131 - Triathlete Talia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAMMY_REWARD": { "label": "Route 121 - Hex Maniac Tammy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TANYA_REWARD": { "label": "Route 125 - Swimmer Tanya", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TARA_REWARD": { "label": "Route 108 - Swimmer Tara", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TASHA_REWARD": { "label": "Mt Pyre 4F - Hex Maniac Tasha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TATE_AND_LIZA_1_REWARD": { "label": "Mossdeep Gym - Leader Tate and Liza", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TAYLOR_REWARD": { "label": "Route 119 - Bug Maniac Taylor", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TYRA_AND_IVY_REWARD": { "label": "Route 114 - Sr. and Jr. Tyra and Ivy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_THALIA_1_REWARD": { "label": "Abandoned Ship 1F - Beauty Thalia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_THOMAS_REWARD": { "label": "SS Tidal - Gentleman Thomas", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIANA_REWARD": { "label": "Route 102 - Lass Tiana", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIFFANY_REWARD": { "label": "Sootopolis Gym - Beauty Tiffany", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIMMY_REWARD": { "label": "Route 110 - Youngster Timmy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TIMOTHY_1_REWARD": { "label": "Route 115 - Expert Timothy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TISHA_REWARD": { "label": "Route 129 - Swimmer Tisha", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TOMMY_REWARD": { "label": "Rustboro Gym - Youngster Tommy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TONY_1_REWARD": { "label": "Route 107 - Swimmer Tony", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TORI_AND_TIA_REWARD": { "label": "Route 113 - Twins Tori and Tia", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TRAVIS_REWARD": { "label": "Route 111 - Camper Travis", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TRENT_1_REWARD": { "label": "Route 112 - Hiker Trent", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_TYRON_REWARD": { "label": "Route 111 - Camper Tyron", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VALERIE_1_REWARD": { "label": "Mt Pyre 6F - Hex Maniac Valerie", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VANESSA_REWARD": { "label": "Route 121 - Pokefan Vanessa", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VICKY_REWARD": { "label": "Route 111 - Winstrate Vicky", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VICTORIA_REWARD": { "label": "Route 111 - Winstrate Victoria", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VICTOR_REWARD": { "label": "Route 111 - Winstrate Victor", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIOLET_REWARD": { "label": "Route 123 - Aroma Lady Violet", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIRGIL_REWARD": { "label": "Mossdeep Gym - Psychic Virgil", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VITO_REWARD": { "label": "Victory Road B2F - Cooltrainer Vito", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIVIAN_REWARD": { "label": "Mauville Gym - Battle Girl Vivian", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_VIVI_REWARD": { "label": "Route 111 - Winstrate Vivi", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WADE_REWARD": { "label": "Route 118 - Fisherman Wade", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALLACE_REWARD": { "label": "Ever Grande City - Champion Wallace", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALTER_1_REWARD": { "label": "Route 121 - Gentleman Walter", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALLY_MAUVILLE_REWARD": { "label": "Mauville City - Rival Wally", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WALLY_VR_1_REWARD": { "label": "Victory Road 1F - Rival Wally", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WATTSON_1_REWARD": { "label": "Mauville Gym - Leader Wattson", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WARREN_REWARD": { "label": "Route 133 - Cooltrainer Warren", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WAYNE_REWARD": { "label": "Route 128 - Fisherman Wayne", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WENDY_REWARD": { "label": "Route 123 - Cooltrainer Wendy", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WILLIAM_REWARD": { "label": "Mt Pyre 3F - Psychic William", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WILTON_1_REWARD": { "label": "Route 111 - Cooltrainer Wilton", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WINONA_1_REWARD": { "label": "Fortree Gym - Leader Winona", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WINSTON_1_REWARD": { "label": "Route 104 - Rich Boy Winston", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_WYATT_REWARD": { "label": "Route 113 - Pokemaniac Wyatt", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_YASU_REWARD": { "label": "Route 119 - Ninja Boy Yasu", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" }, "TRAINER_ZANDER_REWARD": { "label": "Mt Pyre 2F - Black Belt Zander", - "tags": ["Trainer"] + "tags": [], + "category": "TRAINER" } } diff --git a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md index 9a3991e97f75..732b2092a28c 100644 --- a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md +++ b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md @@ -30,7 +30,7 @@ randomizer. Here are some of the more important ones: - The Wally catching tutorial is skipped - All text is instant and, with an option, can be automatically progressed by holding A - When a Repel runs out, you will be prompted to use another -- Many more minor improvementsâ€Ļ +- [Many more minor improvementsâ€Ļ](/tutorial/Pokemon%20Emerald/rom_changes/en) ## Where is my starting inventory? diff --git a/worlds/pokemon_emerald/docs/rom changes.md b/worlds/pokemon_emerald/docs/rom_changes_en.md similarity index 100% rename from worlds/pokemon_emerald/docs/rom changes.md rename to worlds/pokemon_emerald/docs/rom_changes_en.md diff --git a/worlds/pokemon_emerald/docs/setup_sv.md b/worlds/pokemon_emerald/docs/setup_sv.md new file mode 100644 index 000000000000..88b1d384096b --- /dev/null +++ b/worlds/pokemon_emerald/docs/setup_sv.md @@ -0,0 +1,78 @@ +# PokÊmon Emerald Installationsguide + +## Programvara som behÃļvs + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Ett engelskt PokÊmon Emerald ROM, Archipelago kan inte hjälpa dig med detta. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 eller senare + +### Konfigurera BizHawk + +När du har installerat BizHawk, Ãļppna `EmuHawk.exe` och ändra fÃļljande inställningar: + +- Om du använder BizHawk 2.7 eller 2.8, gÃĨ till `Config > Customize`. PÃĨ "Advanced Tab", byt Lua core frÃĨn +`NLua+KopiLua` till `Lua+LuaInterface`, starta om EmuHawk efterÃĨt. (Använder du BizHawk 2.9, kan du skippa detta steg.) +- GÃĨ till `Config > Customize`. Markera "Run in background" inställningen fÃļr att fÃļrhindra bortkoppling frÃĨn +klienten om du alt-tabbar bort frÃĨn EmuHawk. +- Öppna en `.gba` fil i EmuHawk och gÃĨ till `Config > Controllersâ€Ļ` fÃļr att konfigurera dina inputs. +Om du inte hittar `Controllersâ€Ļ`, starta ett valfritt `.gba` ROM fÃļrst. +- Överväg att rensa keybinds i `Config > Hotkeysâ€Ļ` som du inte tänkt använda. Välj en keybind och tryck pÃĨ ESC +fÃļr att rensa bort den. + +## Extra programvara + +- [PokÊmon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), +används tillsammans med +[PopTracker](https://github.com/black-sliver/PopTracker/releases) + +## Generera och patcha ett spel + +1. Skapa din konfigurationsfil (YAML). Du kan gÃļra en via att använda +[PokÊmon Emerald options hemsida](../../../games/Pokemon%20Emerald/player-options). +2. FÃļlj de allmänna Archipelago instruktionerna fÃļr att +[Generera ett spel](../../Archipelago/setup/en#generating-a-game). +Detta kommer generera en fil fÃļr dig. Din patchfil kommer ha `.apemerald` som sitt filnamnstillägg. +3. Öppna `ArchipelagoLauncher.exe` +4. Välj "Open Patch" pÃĨ vänstra sidan, och välj din patchfil. +5. Om detta är fÃļrsta gÃĨngen du patchar, sÃĨ kommer du behÃļva välja var ditt ursprungliga ROM är. +6. En patchad `.gba` fil kommer skapas pÃĨ samma plats som patchfilen. +7. FÃļrsta gÃĨngen du Ãļppnar en patch med BizHawk-klienten, kommer du ocksÃĨ behÃļva bekräfta var `EmuHawk.exe` filen är +installerad i din BizHawk-mapp. + +Om du bara tänkt spela själv och du inte bryr dig om automatisk spÃĨrning eller ledtrÃĨdar, sÃĨ kan du stanna här, stänga +av klienten, och starta ditt patchade ROM med valfri emulator. Dock, fÃļr multvärldsfunktionen eller andra +Archipelago-funktioner, fortsätt nedanfÃļr med BizHawk. + +## Anslut till en server + +Om du vanligtsvis Ãļppnar en patchad fil sÃĨ gÃļrs steg 1-5 automatiskt ÃĨt dig. Även om det är sÃĨ, kom ihÃĨg dessa steg +ifall du till exempel behÃļver stänga ner och starta om nÃĨgot medans du spelar. + +1. Pokemon Emerald använder Archipelagos BizHawk-klient. Om klienten inte startat efter att du patchat ditt spel, +sÃĨ kan du bara Ãļppna den igen frÃĨn launchern. +2. Dubbelkolla att EmuHawk faktiskt startat med den patchade ROM-filen. +3. I EmuHawk, gÃĨ till `Tools > Lua Console`. Luakonsolen mÃĨste vara igÃĨng medans du spelar. +4. I Luakonsolen, Tryck pÃĨ `Script > Open Scriptâ€Ļ`. +5. Leta reda pÃĨ din Archipelago-mapp och i den Ãļppna `data/lua/connector_bizhawk_generic.lua`. +6. Emulatorn och klienten kommer sÃĨ smÃĨningom ansluta till varandra. I BizHawk-klienten kommer du kunna see om allt är +anslutet och att Pokemon Emerald är igenkänt. +7. FÃļr att ansluta klienten till en server, skriv in din lobbyadress och port i textfältet t.ex. +`archipelago.gg:38281` +längst upp i din klient och tryck sen pÃĨ "Connect". + +Du borde nu kunna ta emot och skicka fÃļremÃĨl. Du behÃļver gÃļra dom här stegen varje gÃĨng du vill ansluta igen. Det är +helt okej att gÃļra saker offline utan att behÃļva oroa sig; allt kommer att synkronisera när du ansluter till servern +igen. + +## Automatisk SpÃĨrning + +PokÊmon Emerald har en fullt fungerande spÃĨrare med stÃļd fÃļr automatisk spÃĨrning. + +1. Ladda ner [PokÊmon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) +och +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Placera tracker pack zip-filen i packs/ där du har PopTracker installerat. +3. Öppna PopTracker, och välj Pokemon Emerald. +4. FÃļr att automatiskt spÃĨra, tryck pÃĨ "AP" symbolen längst upp. +5. Skriv in Archipelago-serverns uppgifter (Samma som du använde fÃļr att ansluta med klienten), "Slot"-namn samt +lÃļsenord. diff --git a/worlds/pokemon_emerald/groups.py b/worlds/pokemon_emerald/groups.py new file mode 100644 index 000000000000..d358da18350f --- /dev/null +++ b/worlds/pokemon_emerald/groups.py @@ -0,0 +1,721 @@ +from typing import Dict, Set + +from .data import LocationCategory, data + + +# Item Groups +ITEM_GROUPS: Dict[str, Set[str]] = {} + +for item in data.items.values(): + for tag in item.tags: + if tag not in ITEM_GROUPS: + ITEM_GROUPS[tag] = set() + ITEM_GROUPS[tag].add(item.label) + +# Location Groups +_LOCATION_GROUP_MAPS = { + "Abandoned Ship": { + "MAP_ABANDONED_SHIP_CAPTAINS_OFFICE", + "MAP_ABANDONED_SHIP_CORRIDORS_1F", + "MAP_ABANDONED_SHIP_CORRIDORS_B1F", + "MAP_ABANDONED_SHIP_DECK", + "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS", + "MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS", + "MAP_ABANDONED_SHIP_ROOMS2_1F", + "MAP_ABANDONED_SHIP_ROOMS2_B1F", + "MAP_ABANDONED_SHIP_ROOMS_1F", + "MAP_ABANDONED_SHIP_ROOMS_B1F", + "MAP_ABANDONED_SHIP_ROOM_B1F", + "MAP_ABANDONED_SHIP_UNDERWATER1", + "MAP_ABANDONED_SHIP_UNDERWATER2", + }, + "Aqua Hideout": { + "MAP_AQUA_HIDEOUT_1F", + "MAP_AQUA_HIDEOUT_B1F", + "MAP_AQUA_HIDEOUT_B2F", + }, + "Battle Frontier": { + "MAP_ARTISAN_CAVE_1F", + "MAP_ARTISAN_CAVE_B1F", + "MAP_BATTLE_FRONTIER_BATTLE_ARENA_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_ARENA_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_PRE_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_FINAL", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_NORMAL", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_WILD_MONS", + "MAP_BATTLE_FRONTIER_BATTLE_PIKE_THREE_PATH_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_FLOOR", + "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_TOP", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_ELEVATOR", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_BATTLE_ROOM", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_CORRIDOR", + "MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_PARTNER_ROOM", + "MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER", + "MAP_BATTLE_FRONTIER_LOUNGE1", + "MAP_BATTLE_FRONTIER_LOUNGE2", + "MAP_BATTLE_FRONTIER_LOUNGE3", + "MAP_BATTLE_FRONTIER_LOUNGE4", + "MAP_BATTLE_FRONTIER_LOUNGE5", + "MAP_BATTLE_FRONTIER_LOUNGE6", + "MAP_BATTLE_FRONTIER_LOUNGE7", + "MAP_BATTLE_FRONTIER_LOUNGE8", + "MAP_BATTLE_FRONTIER_LOUNGE9", + "MAP_BATTLE_FRONTIER_MART", + "MAP_BATTLE_FRONTIER_OUTSIDE_EAST", + "MAP_BATTLE_FRONTIER_OUTSIDE_WEST", + "MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F", + "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F", + "MAP_BATTLE_FRONTIER_RANKING_HALL", + "MAP_BATTLE_FRONTIER_RECEPTION_GATE", + "MAP_BATTLE_FRONTIER_SCOTTS_HOUSE", + "MAP_BATTLE_PYRAMID_SQUARE01", + "MAP_BATTLE_PYRAMID_SQUARE02", + "MAP_BATTLE_PYRAMID_SQUARE03", + "MAP_BATTLE_PYRAMID_SQUARE04", + "MAP_BATTLE_PYRAMID_SQUARE05", + "MAP_BATTLE_PYRAMID_SQUARE06", + "MAP_BATTLE_PYRAMID_SQUARE07", + "MAP_BATTLE_PYRAMID_SQUARE08", + "MAP_BATTLE_PYRAMID_SQUARE09", + "MAP_BATTLE_PYRAMID_SQUARE10", + "MAP_BATTLE_PYRAMID_SQUARE11", + "MAP_BATTLE_PYRAMID_SQUARE12", + "MAP_BATTLE_PYRAMID_SQUARE13", + "MAP_BATTLE_PYRAMID_SQUARE14", + "MAP_BATTLE_PYRAMID_SQUARE15", + "MAP_BATTLE_PYRAMID_SQUARE16", + }, + "Birth Island": { + "MAP_BIRTH_ISLAND_EXTERIOR", + "MAP_BIRTH_ISLAND_HARBOR", + }, + "Contest Hall": { + "MAP_CONTEST_HALL", + "MAP_CONTEST_HALL_BEAUTY", + "MAP_CONTEST_HALL_COOL", + "MAP_CONTEST_HALL_CUTE", + "MAP_CONTEST_HALL_SMART", + "MAP_CONTEST_HALL_TOUGH", + }, + "Dewford Town": { + "MAP_DEWFORD_TOWN", + "MAP_DEWFORD_TOWN_GYM", + "MAP_DEWFORD_TOWN_HALL", + "MAP_DEWFORD_TOWN_HOUSE1", + "MAP_DEWFORD_TOWN_HOUSE2", + "MAP_DEWFORD_TOWN_POKEMON_CENTER_1F", + "MAP_DEWFORD_TOWN_POKEMON_CENTER_2F", + }, + "Ever Grande City": { + "MAP_EVER_GRANDE_CITY", + "MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM", + "MAP_EVER_GRANDE_CITY_DRAKES_ROOM", + "MAP_EVER_GRANDE_CITY_GLACIAS_ROOM", + "MAP_EVER_GRANDE_CITY_HALL1", + "MAP_EVER_GRANDE_CITY_HALL2", + "MAP_EVER_GRANDE_CITY_HALL3", + "MAP_EVER_GRANDE_CITY_HALL4", + "MAP_EVER_GRANDE_CITY_HALL5", + "MAP_EVER_GRANDE_CITY_HALL_OF_FAME", + "MAP_EVER_GRANDE_CITY_PHOEBES_ROOM", + "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F", + "MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F", + "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F", + "MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F", + "MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM", + }, + "Fallarbor Town": { + "MAP_FALLARBOR_TOWN", + "MAP_FALLARBOR_TOWN_BATTLE_TENT_BATTLE_ROOM", + "MAP_FALLARBOR_TOWN_BATTLE_TENT_CORRIDOR", + "MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY", + "MAP_FALLARBOR_TOWN_COZMOS_HOUSE", + "MAP_FALLARBOR_TOWN_MART", + "MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE", + "MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F", + "MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F", + }, + "Faraway Island": { + "MAP_FARAWAY_ISLAND_ENTRANCE", + "MAP_FARAWAY_ISLAND_INTERIOR", + }, + "Fiery Path": {"MAP_FIERY_PATH"}, + "Fortree City": { + "MAP_FORTREE_CITY", + "MAP_FORTREE_CITY_DECORATION_SHOP", + "MAP_FORTREE_CITY_GYM", + "MAP_FORTREE_CITY_HOUSE1", + "MAP_FORTREE_CITY_HOUSE2", + "MAP_FORTREE_CITY_HOUSE3", + "MAP_FORTREE_CITY_HOUSE4", + "MAP_FORTREE_CITY_HOUSE5", + "MAP_FORTREE_CITY_MART", + "MAP_FORTREE_CITY_POKEMON_CENTER_1F", + "MAP_FORTREE_CITY_POKEMON_CENTER_2F", + }, + "Granite Cave": { + "MAP_GRANITE_CAVE_1F", + "MAP_GRANITE_CAVE_B1F", + "MAP_GRANITE_CAVE_B2F", + "MAP_GRANITE_CAVE_STEVENS_ROOM", + }, + "Jagged Pass": {"MAP_JAGGED_PASS"}, + "Lavaridge Town": { + "MAP_LAVARIDGE_TOWN", + "MAP_LAVARIDGE_TOWN_GYM_1F", + "MAP_LAVARIDGE_TOWN_GYM_B1F", + "MAP_LAVARIDGE_TOWN_HERB_SHOP", + "MAP_LAVARIDGE_TOWN_HOUSE", + "MAP_LAVARIDGE_TOWN_MART", + "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F", + "MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F", + }, + "Lilycove City": { + "MAP_LILYCOVE_CITY", + "MAP_LILYCOVE_CITY_CONTEST_HALL", + "MAP_LILYCOVE_CITY_CONTEST_LOBBY", + "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F", + "MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR", + "MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP", + "MAP_LILYCOVE_CITY_HARBOR", + "MAP_LILYCOVE_CITY_HOUSE1", + "MAP_LILYCOVE_CITY_HOUSE2", + "MAP_LILYCOVE_CITY_HOUSE3", + "MAP_LILYCOVE_CITY_HOUSE4", + "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F", + "MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F", + "MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE", + "MAP_LILYCOVE_CITY_POKEMON_CENTER_1F", + "MAP_LILYCOVE_CITY_POKEMON_CENTER_2F", + "MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB", + }, + "Littleroot Town": { + "MAP_INSIDE_OF_TRUCK", + "MAP_LITTLEROOT_TOWN", + "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F", + "MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F", + "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F", + "MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F", + "MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB", + }, + "Magma Hideout": { + "MAP_MAGMA_HIDEOUT_1F", + "MAP_MAGMA_HIDEOUT_2F_1R", + "MAP_MAGMA_HIDEOUT_2F_2R", + "MAP_MAGMA_HIDEOUT_2F_3R", + "MAP_MAGMA_HIDEOUT_3F_1R", + "MAP_MAGMA_HIDEOUT_3F_2R", + "MAP_MAGMA_HIDEOUT_3F_3R", + "MAP_MAGMA_HIDEOUT_4F", + }, + "Marine Cave": { + "MAP_MARINE_CAVE_END", + "MAP_MARINE_CAVE_ENTRANCE", + "MAP_UNDERWATER_MARINE_CAVE", + }, + "Mauville City": { + "MAP_MAUVILLE_CITY", + "MAP_MAUVILLE_CITY_BIKE_SHOP", + "MAP_MAUVILLE_CITY_GAME_CORNER", + "MAP_MAUVILLE_CITY_GYM", + "MAP_MAUVILLE_CITY_HOUSE1", + "MAP_MAUVILLE_CITY_HOUSE2", + "MAP_MAUVILLE_CITY_MART", + "MAP_MAUVILLE_CITY_POKEMON_CENTER_1F", + "MAP_MAUVILLE_CITY_POKEMON_CENTER_2F", + }, + "Meteor Falls": { + "MAP_METEOR_FALLS_1F_1R", + "MAP_METEOR_FALLS_1F_2R", + "MAP_METEOR_FALLS_B1F_1R", + "MAP_METEOR_FALLS_B1F_2R", + "MAP_METEOR_FALLS_STEVENS_CAVE", + }, + "Mirage Tower": { + "MAP_MIRAGE_TOWER_1F", + "MAP_MIRAGE_TOWER_2F", + "MAP_MIRAGE_TOWER_3F", + "MAP_MIRAGE_TOWER_4F", + }, + "Mossdeep City": { + "MAP_MOSSDEEP_CITY", + "MAP_MOSSDEEP_CITY_GAME_CORNER_1F", + "MAP_MOSSDEEP_CITY_GAME_CORNER_B1F", + "MAP_MOSSDEEP_CITY_GYM", + "MAP_MOSSDEEP_CITY_HOUSE1", + "MAP_MOSSDEEP_CITY_HOUSE2", + "MAP_MOSSDEEP_CITY_HOUSE3", + "MAP_MOSSDEEP_CITY_HOUSE4", + "MAP_MOSSDEEP_CITY_MART", + "MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F", + "MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F", + "MAP_MOSSDEEP_CITY_SPACE_CENTER_1F", + "MAP_MOSSDEEP_CITY_SPACE_CENTER_2F", + "MAP_MOSSDEEP_CITY_STEVENS_HOUSE", + }, + "Mt. Chimney": { + "MAP_MT_CHIMNEY", + "MAP_MT_CHIMNEY_CABLE_CAR_STATION", + }, + "Mt. Pyre": { + "MAP_MT_PYRE_1F", + "MAP_MT_PYRE_2F", + "MAP_MT_PYRE_3F", + "MAP_MT_PYRE_4F", + "MAP_MT_PYRE_5F", + "MAP_MT_PYRE_6F", + "MAP_MT_PYRE_EXTERIOR", + "MAP_MT_PYRE_SUMMIT", + }, + "Navel Rock": { + "MAP_NAVEL_ROCK_B1F", + "MAP_NAVEL_ROCK_BOTTOM", + "MAP_NAVEL_ROCK_DOWN01", + "MAP_NAVEL_ROCK_DOWN02", + "MAP_NAVEL_ROCK_DOWN03", + "MAP_NAVEL_ROCK_DOWN04", + "MAP_NAVEL_ROCK_DOWN05", + "MAP_NAVEL_ROCK_DOWN06", + "MAP_NAVEL_ROCK_DOWN07", + "MAP_NAVEL_ROCK_DOWN08", + "MAP_NAVEL_ROCK_DOWN09", + "MAP_NAVEL_ROCK_DOWN10", + "MAP_NAVEL_ROCK_DOWN11", + "MAP_NAVEL_ROCK_ENTRANCE", + "MAP_NAVEL_ROCK_EXTERIOR", + "MAP_NAVEL_ROCK_FORK", + "MAP_NAVEL_ROCK_HARBOR", + "MAP_NAVEL_ROCK_TOP", + "MAP_NAVEL_ROCK_UP1", + "MAP_NAVEL_ROCK_UP2", + "MAP_NAVEL_ROCK_UP3", + "MAP_NAVEL_ROCK_UP4", + }, + "New Mauville": { + "MAP_NEW_MAUVILLE_ENTRANCE", + "MAP_NEW_MAUVILLE_INSIDE", + }, + "Oldale Town": { + "MAP_OLDALE_TOWN", + "MAP_OLDALE_TOWN_HOUSE1", + "MAP_OLDALE_TOWN_HOUSE2", + "MAP_OLDALE_TOWN_MART", + "MAP_OLDALE_TOWN_POKEMON_CENTER_1F", + "MAP_OLDALE_TOWN_POKEMON_CENTER_2F", + }, + "Pacifidlog Town": { + "MAP_PACIFIDLOG_TOWN", + "MAP_PACIFIDLOG_TOWN_HOUSE1", + "MAP_PACIFIDLOG_TOWN_HOUSE2", + "MAP_PACIFIDLOG_TOWN_HOUSE3", + "MAP_PACIFIDLOG_TOWN_HOUSE4", + "MAP_PACIFIDLOG_TOWN_HOUSE5", + "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F", + "MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F", + }, + "Petalburg City": { + "MAP_PETALBURG_CITY", + "MAP_PETALBURG_CITY_GYM", + "MAP_PETALBURG_CITY_HOUSE1", + "MAP_PETALBURG_CITY_HOUSE2", + "MAP_PETALBURG_CITY_MART", + "MAP_PETALBURG_CITY_POKEMON_CENTER_1F", + "MAP_PETALBURG_CITY_POKEMON_CENTER_2F", + "MAP_PETALBURG_CITY_WALLYS_HOUSE", + }, + "Petalburg Woods": {"MAP_PETALBURG_WOODS"}, + "Route 101": {"MAP_ROUTE101"}, + "Route 102": {"MAP_ROUTE102"}, + "Route 103": {"MAP_ROUTE103"}, + "Route 104": { + "MAP_ROUTE104", + "MAP_ROUTE104_MR_BRINEYS_HOUSE", + "MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP", + }, + "Route 105": { + "MAP_ISLAND_CAVE", + "MAP_ROUTE105", + "MAP_UNDERWATER_ROUTE105", + }, + "Route 106": {"MAP_ROUTE106"}, + "Route 107": {"MAP_ROUTE107"}, + "Route 108": {"MAP_ROUTE108"}, + "Route 109": { + "MAP_ROUTE109", + "MAP_ROUTE109_SEASHORE_HOUSE", + }, + "Route 110": { + "MAP_ROUTE110", + "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE", + "MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE", + }, + "Trick House": { + "MAP_ROUTE110_TRICK_HOUSE_CORRIDOR", + "MAP_ROUTE110_TRICK_HOUSE_END", + "MAP_ROUTE110_TRICK_HOUSE_ENTRANCE", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE1", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7", + "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8", + }, + "Route 111": { + "MAP_DESERT_RUINS", + "MAP_ROUTE111", + "MAP_ROUTE111_OLD_LADYS_REST_STOP", + "MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE", + }, + "Route 112": { + "MAP_ROUTE112", + "MAP_ROUTE112_CABLE_CAR_STATION", + }, + "Route 113": { + "MAP_ROUTE113", + "MAP_ROUTE113_GLASS_WORKSHOP", + }, + "Route 114": { + "MAP_DESERT_UNDERPASS", + "MAP_ROUTE114", + "MAP_ROUTE114_FOSSIL_MANIACS_HOUSE", + "MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL", + "MAP_ROUTE114_LANETTES_HOUSE", + }, + "Route 115": {"MAP_ROUTE115"}, + "Route 116": { + "MAP_ROUTE116", + "MAP_ROUTE116_TUNNELERS_REST_HOUSE", + }, + "Route 117": { + "MAP_ROUTE117", + "MAP_ROUTE117_POKEMON_DAY_CARE", + }, + "Route 118": {"MAP_ROUTE118"}, + "Route 119": { + "MAP_ROUTE119", + "MAP_ROUTE119_HOUSE", + "MAP_ROUTE119_WEATHER_INSTITUTE_1F", + "MAP_ROUTE119_WEATHER_INSTITUTE_2F", + }, + "Route 120": { + "MAP_ANCIENT_TOMB", + "MAP_ROUTE120", + "MAP_SCORCHED_SLAB", + }, + "Route 121": { + "MAP_ROUTE121", + }, + "Route 122": {"MAP_ROUTE122"}, + "Route 123": { + "MAP_ROUTE123", + "MAP_ROUTE123_BERRY_MASTERS_HOUSE", + }, + "Route 124": { + "MAP_ROUTE124", + "MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE", + "MAP_UNDERWATER_ROUTE124", + }, + "Route 125": { + "MAP_ROUTE125", + "MAP_UNDERWATER_ROUTE125", + }, + "Route 126": { + "MAP_ROUTE126", + "MAP_UNDERWATER_ROUTE126", + }, + "Route 127": { + "MAP_ROUTE127", + "MAP_UNDERWATER_ROUTE127", + }, + "Route 128": { + "MAP_ROUTE128", + "MAP_UNDERWATER_ROUTE128", + }, + "Route 129": { + "MAP_ROUTE129", + "MAP_UNDERWATER_ROUTE129", + }, + "Route 130": {"MAP_ROUTE130"}, + "Route 131": {"MAP_ROUTE131"}, + "Route 132": {"MAP_ROUTE132"}, + "Route 133": {"MAP_ROUTE133"}, + "Route 134": { + "MAP_ROUTE134", + "MAP_UNDERWATER_ROUTE134", + "MAP_SEALED_CHAMBER_INNER_ROOM", + "MAP_SEALED_CHAMBER_OUTER_ROOM", + "MAP_UNDERWATER_SEALED_CHAMBER", + }, + "Rustboro City": { + "MAP_RUSTBORO_CITY", + "MAP_RUSTBORO_CITY_CUTTERS_HOUSE", + "MAP_RUSTBORO_CITY_DEVON_CORP_1F", + "MAP_RUSTBORO_CITY_DEVON_CORP_2F", + "MAP_RUSTBORO_CITY_DEVON_CORP_3F", + "MAP_RUSTBORO_CITY_FLAT1_1F", + "MAP_RUSTBORO_CITY_FLAT1_2F", + "MAP_RUSTBORO_CITY_FLAT2_1F", + "MAP_RUSTBORO_CITY_FLAT2_2F", + "MAP_RUSTBORO_CITY_FLAT2_3F", + "MAP_RUSTBORO_CITY_GYM", + "MAP_RUSTBORO_CITY_HOUSE1", + "MAP_RUSTBORO_CITY_HOUSE2", + "MAP_RUSTBORO_CITY_HOUSE3", + "MAP_RUSTBORO_CITY_MART", + "MAP_RUSTBORO_CITY_POKEMON_CENTER_1F", + "MAP_RUSTBORO_CITY_POKEMON_CENTER_2F", + "MAP_RUSTBORO_CITY_POKEMON_SCHOOL", + }, + "Rusturf Tunnel": {"MAP_RUSTURF_TUNNEL"}, + "Safari Zone": { + "MAP_ROUTE121_SAFARI_ZONE_ENTRANCE", + "MAP_SAFARI_ZONE_NORTH", + "MAP_SAFARI_ZONE_NORTHEAST", + "MAP_SAFARI_ZONE_NORTHWEST", + "MAP_SAFARI_ZONE_REST_HOUSE", + "MAP_SAFARI_ZONE_SOUTH", + "MAP_SAFARI_ZONE_SOUTHEAST", + "MAP_SAFARI_ZONE_SOUTHWEST", + }, + "Seafloor Cavern": { + "MAP_SEAFLOOR_CAVERN_ENTRANCE", + "MAP_SEAFLOOR_CAVERN_ROOM1", + "MAP_SEAFLOOR_CAVERN_ROOM2", + "MAP_SEAFLOOR_CAVERN_ROOM3", + "MAP_SEAFLOOR_CAVERN_ROOM4", + "MAP_SEAFLOOR_CAVERN_ROOM5", + "MAP_SEAFLOOR_CAVERN_ROOM6", + "MAP_SEAFLOOR_CAVERN_ROOM7", + "MAP_SEAFLOOR_CAVERN_ROOM8", + "MAP_SEAFLOOR_CAVERN_ROOM9", + "MAP_UNDERWATER_SEAFLOOR_CAVERN", + }, + "Shoal Cave": { + "MAP_SHOAL_CAVE_HIGH_TIDE_ENTRANCE_ROOM", + "MAP_SHOAL_CAVE_HIGH_TIDE_INNER_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM", + }, + "Sky Pillar": { + "MAP_SKY_PILLAR_1F", + "MAP_SKY_PILLAR_2F", + "MAP_SKY_PILLAR_3F", + "MAP_SKY_PILLAR_4F", + "MAP_SKY_PILLAR_5F", + "MAP_SKY_PILLAR_ENTRANCE", + "MAP_SKY_PILLAR_OUTSIDE", + "MAP_SKY_PILLAR_TOP", + }, + "Slateport City": { + "MAP_SLATEPORT_CITY", + "MAP_SLATEPORT_CITY_BATTLE_TENT_BATTLE_ROOM", + "MAP_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR", + "MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY", + "MAP_SLATEPORT_CITY_HARBOR", + "MAP_SLATEPORT_CITY_HOUSE", + "MAP_SLATEPORT_CITY_MART", + "MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE", + "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F", + "MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F", + "MAP_SLATEPORT_CITY_POKEMON_CENTER_1F", + "MAP_SLATEPORT_CITY_POKEMON_CENTER_2F", + "MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB", + "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F", + "MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F", + }, + "Sootopolis City": { + "MAP_CAVE_OF_ORIGIN_1F", + "MAP_CAVE_OF_ORIGIN_B1F", + "MAP_CAVE_OF_ORIGIN_ENTRANCE", + "MAP_SOOTOPOLIS_CITY", + "MAP_SOOTOPOLIS_CITY_GYM_1F", + "MAP_SOOTOPOLIS_CITY_GYM_B1F", + "MAP_SOOTOPOLIS_CITY_HOUSE1", + "MAP_SOOTOPOLIS_CITY_HOUSE2", + "MAP_SOOTOPOLIS_CITY_HOUSE3", + "MAP_SOOTOPOLIS_CITY_HOUSE4", + "MAP_SOOTOPOLIS_CITY_HOUSE5", + "MAP_SOOTOPOLIS_CITY_HOUSE6", + "MAP_SOOTOPOLIS_CITY_HOUSE7", + "MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE", + "MAP_SOOTOPOLIS_CITY_MART", + "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F", + "MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F", + "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F", + "MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F", + "MAP_UNDERWATER_SOOTOPOLIS_CITY", + }, + "Southern Island": { + "MAP_SOUTHERN_ISLAND_EXTERIOR", + "MAP_SOUTHERN_ISLAND_INTERIOR", + }, + "S.S. Tidal": { + "MAP_SS_TIDAL_CORRIDOR", + "MAP_SS_TIDAL_LOWER_DECK", + "MAP_SS_TIDAL_ROOMS", + }, + "Terra Cave": { + "MAP_TERRA_CAVE_END", + "MAP_TERRA_CAVE_ENTRANCE", + }, + "Trainer Hill": { + "MAP_TRAINER_HILL_2F", + "MAP_TRAINER_HILL_3F", + "MAP_TRAINER_HILL_4F", + "MAP_TRAINER_HILL_ELEVATOR", + "MAP_TRAINER_HILL_ENTRANCE", + "MAP_TRAINER_HILL_ROOF", + }, + "Verdanturf Town": { + "MAP_VERDANTURF_TOWN", + "MAP_VERDANTURF_TOWN_BATTLE_TENT_BATTLE_ROOM", + "MAP_VERDANTURF_TOWN_BATTLE_TENT_CORRIDOR", + "MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY", + "MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE", + "MAP_VERDANTURF_TOWN_HOUSE", + "MAP_VERDANTURF_TOWN_MART", + "MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F", + "MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F", + "MAP_VERDANTURF_TOWN_WANDAS_HOUSE", + }, + "Victory Road": { + "MAP_VICTORY_ROAD_1F", + "MAP_VICTORY_ROAD_B1F", + "MAP_VICTORY_ROAD_B2F", + }, +} + +_LOCATION_CATEGORY_TO_GROUP_NAME = { + LocationCategory.BADGE: "Badges", + LocationCategory.HM: "HMs", + LocationCategory.KEY: "Key Items", + LocationCategory.ROD: "Fishing Rods", + LocationCategory.BIKE: "Bikes", + LocationCategory.TICKET: "Tickets", + LocationCategory.OVERWORLD_ITEM: "Overworld Items", + LocationCategory.HIDDEN_ITEM: "Hidden Items", + LocationCategory.GIFT: "NPC Gifts", + LocationCategory.BERRY_TREE: "Berry Trees", + LocationCategory.TRAINER: "Trainers", + LocationCategory.POKEDEX: "Pokedex", +} + +LOCATION_GROUPS: Dict[str, Set[str]] = {group_name: set() for group_name in _LOCATION_CATEGORY_TO_GROUP_NAME.values()} +for location in data.locations.values(): + # Category groups + LOCATION_GROUPS[_LOCATION_CATEGORY_TO_GROUP_NAME[location.category]].add(location.label) + + # Tag groups + for tag in location.tags: + if tag not in LOCATION_GROUPS: + LOCATION_GROUPS[tag] = set() + LOCATION_GROUPS[tag].add(location.label) + + # Geographic groups + if location.parent_region != "REGION_POKEDEX": + map_name = data.regions[location.parent_region].parent_map.name + for group, maps in _LOCATION_GROUP_MAPS.items(): + if map_name in maps: + if group not in LOCATION_GROUPS: + LOCATION_GROUPS[group] = set() + LOCATION_GROUPS[group].add(location.label) + break + +# Meta-groups +LOCATION_GROUPS["Cities"] = { + *LOCATION_GROUPS.get("Littleroot Town", set()), + *LOCATION_GROUPS.get("Oldale Town", set()), + *LOCATION_GROUPS.get("Petalburg City", set()), + *LOCATION_GROUPS.get("Rustboro City", set()), + *LOCATION_GROUPS.get("Dewford Town", set()), + *LOCATION_GROUPS.get("Slateport City", set()), + *LOCATION_GROUPS.get("Mauville City", set()), + *LOCATION_GROUPS.get("Verdanturf Town", set()), + *LOCATION_GROUPS.get("Fallarbor Town", set()), + *LOCATION_GROUPS.get("Lavaridge Town", set()), + *LOCATION_GROUPS.get("Fortree City", set()), + *LOCATION_GROUPS.get("Mossdeep City", set()), + *LOCATION_GROUPS.get("Sootopolis City", set()), + *LOCATION_GROUPS.get("Pacifidlog Town", set()), + *LOCATION_GROUPS.get("Ever Grande City", set()), +} + +LOCATION_GROUPS["Dungeons"] = { + *LOCATION_GROUPS.get("Petalburg Woods", set()), + *LOCATION_GROUPS.get("Rusturf Tunnel", set()), + *LOCATION_GROUPS.get("Granite Cave", set()), + *LOCATION_GROUPS.get("Fiery Path", set()), + *LOCATION_GROUPS.get("Meteor Falls", set()), + *LOCATION_GROUPS.get("Jagged Pass", set()), + *LOCATION_GROUPS.get("Mt. Chimney", set()), + *LOCATION_GROUPS.get("Abandoned Ship", set()), + *LOCATION_GROUPS.get("New Mauville", set()), + *LOCATION_GROUPS.get("Mt. Pyre", set()), + *LOCATION_GROUPS.get("Seafloor Cavern", set()), + *LOCATION_GROUPS.get("Sky Pillar", set()), + *LOCATION_GROUPS.get("Victory Road", set()), +} + +LOCATION_GROUPS["Routes"] = { + *LOCATION_GROUPS.get("Route 101", set()), + *LOCATION_GROUPS.get("Route 102", set()), + *LOCATION_GROUPS.get("Route 103", set()), + *LOCATION_GROUPS.get("Route 104", set()), + *LOCATION_GROUPS.get("Route 105", set()), + *LOCATION_GROUPS.get("Route 106", set()), + *LOCATION_GROUPS.get("Route 107", set()), + *LOCATION_GROUPS.get("Route 108", set()), + *LOCATION_GROUPS.get("Route 109", set()), + *LOCATION_GROUPS.get("Route 110", set()), + *LOCATION_GROUPS.get("Route 111", set()), + *LOCATION_GROUPS.get("Route 112", set()), + *LOCATION_GROUPS.get("Route 113", set()), + *LOCATION_GROUPS.get("Route 114", set()), + *LOCATION_GROUPS.get("Route 115", set()), + *LOCATION_GROUPS.get("Route 116", set()), + *LOCATION_GROUPS.get("Route 117", set()), + *LOCATION_GROUPS.get("Route 118", set()), + *LOCATION_GROUPS.get("Route 119", set()), + *LOCATION_GROUPS.get("Route 120", set()), + *LOCATION_GROUPS.get("Route 121", set()), + *LOCATION_GROUPS.get("Route 122", set()), + *LOCATION_GROUPS.get("Route 123", set()), + *LOCATION_GROUPS.get("Route 124", set()), + *LOCATION_GROUPS.get("Route 125", set()), + *LOCATION_GROUPS.get("Route 126", set()), + *LOCATION_GROUPS.get("Route 127", set()), + *LOCATION_GROUPS.get("Route 128", set()), + *LOCATION_GROUPS.get("Route 129", set()), + *LOCATION_GROUPS.get("Route 130", set()), + *LOCATION_GROUPS.get("Route 131", set()), + *LOCATION_GROUPS.get("Route 132", set()), + *LOCATION_GROUPS.get("Route 133", set()), + *LOCATION_GROUPS.get("Route 134", set()), +} diff --git a/worlds/pokemon_emerald/items.py b/worlds/pokemon_emerald/items.py index 436db771d396..922bbbc0dbfb 100644 --- a/worlds/pokemon_emerald/items.py +++ b/worlds/pokemon_emerald/items.py @@ -1,7 +1,7 @@ """ Classes and functions related to AP items for Pokemon Emerald """ -from typing import Dict, FrozenSet, Optional +from typing import Dict, FrozenSet, Set, Optional from BaseClasses import Item, ItemClassification @@ -46,30 +46,6 @@ def create_item_label_to_code_map() -> Dict[str, int]: return label_to_code_map -ITEM_GROUPS = { - "Badges": { - "Stone Badge", "Knuckle Badge", - "Dynamo Badge", "Heat Badge", - "Balance Badge", "Feather Badge", - "Mind Badge", "Rain Badge", - }, - "HMs": { - "HM01 Cut", "HM02 Fly", - "HM03 Surf", "HM04 Strength", - "HM05 Flash", "HM06 Rock Smash", - "HM07 Waterfall", "HM08 Dive", - }, - "HM01": {"HM01 Cut"}, - "HM02": {"HM02 Fly"}, - "HM03": {"HM03 Surf"}, - "HM04": {"HM04 Strength"}, - "HM05": {"HM05 Flash"}, - "HM06": {"HM06 Rock Smash"}, - "HM07": {"HM07 Waterfall"}, - "HM08": {"HM08 Dive"}, -} - - def get_item_classification(item_code: int) -> ItemClassification: """ Returns the item classification for a given AP item id (code) diff --git a/worlds/pokemon_emerald/locations.py b/worlds/pokemon_emerald/locations.py index 9123690bead7..473c189166be 100644 --- a/worlds/pokemon_emerald/locations.py +++ b/worlds/pokemon_emerald/locations.py @@ -1,59 +1,17 @@ """ Classes and functions related to AP locations for Pokemon Emerald """ -from typing import TYPE_CHECKING, Dict, Optional, FrozenSet, Iterable +from typing import TYPE_CHECKING, Dict, Optional, Set from BaseClasses import Location, Region -from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, data +from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, LocationCategory, data from .items import offset_item_value if TYPE_CHECKING: from . import PokemonEmeraldWorld -LOCATION_GROUPS = { - "Badges": { - "Rustboro Gym - Stone Badge", - "Dewford Gym - Knuckle Badge", - "Mauville Gym - Dynamo Badge", - "Lavaridge Gym - Heat Badge", - "Petalburg Gym - Balance Badge", - "Fortree Gym - Feather Badge", - "Mossdeep Gym - Mind Badge", - "Sootopolis Gym - Rain Badge", - }, - "Gym TMs": { - "Rustboro Gym - TM39 from Roxanne", - "Dewford Gym - TM08 from Brawly", - "Mauville Gym - TM34 from Wattson", - "Lavaridge Gym - TM50 from Flannery", - "Petalburg Gym - TM42 from Norman", - "Fortree Gym - TM40 from Winona", - "Mossdeep Gym - TM04 from Tate and Liza", - "Sootopolis Gym - TM03 from Juan", - }, - "Trick House": { - "Trick House Puzzle 1 - Item", - "Trick House Puzzle 2 - Item 1", - "Trick House Puzzle 2 - Item 2", - "Trick House Puzzle 3 - Item 1", - "Trick House Puzzle 3 - Item 2", - "Trick House Puzzle 4 - Item", - "Trick House Puzzle 6 - Item", - "Trick House Puzzle 7 - Item", - "Trick House Puzzle 8 - Item", - "Trick House Puzzle 1 - Reward", - "Trick House Puzzle 2 - Reward", - "Trick House Puzzle 3 - Reward", - "Trick House Puzzle 4 - Reward", - "Trick House Puzzle 5 - Reward", - "Trick House Puzzle 6 - Reward", - "Trick House Puzzle 7 - Reward", - } -} - - VISITED_EVENT_NAME_TO_ID = { "EVENT_VISITED_LITTLEROOT_TOWN": 0, "EVENT_VISITED_OLDALE_TOWN": 1, @@ -80,7 +38,7 @@ class PokemonEmeraldLocation(Location): game: str = "Pokemon Emerald" item_address: Optional[int] default_item_code: Optional[int] - tags: FrozenSet[str] + key: Optional[str] def __init__( self, @@ -88,13 +46,13 @@ def __init__( name: str, address: Optional[int], parent: Optional[Region] = None, + key: Optional[str] = None, item_address: Optional[int] = None, - default_item_value: Optional[int] = None, - tags: FrozenSet[str] = frozenset()) -> None: + default_item_value: Optional[int] = None) -> None: super().__init__(player, name, address, parent) self.default_item_code = None if default_item_value is None else offset_item_value(default_item_value) self.item_address = item_address - self.tags = tags + self.key = key def offset_flag(flag: int) -> int: @@ -115,16 +73,14 @@ def reverse_offset_flag(location_id: int) -> int: return location_id - BASE_OFFSET -def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, Region], tags: Iterable[str]) -> None: +def create_locations_by_category(world: "PokemonEmeraldWorld", regions: Dict[str, Region], categories: Set[LocationCategory]) -> None: """ Iterates through region data and adds locations to the multiworld if those locations include any of the provided tags. """ - tags = set(tags) - for region_name, region_data in data.regions.items(): region = regions[region_name] - filtered_locations = [loc for loc in region_data.locations if len(tags & data.locations[loc].tags) > 0] + filtered_locations = [loc for loc in region_data.locations if data.locations[loc].category in categories] for location_name in filtered_locations: location_data = data.locations[location_name] @@ -144,9 +100,9 @@ def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, location_data.label, location_id, region, + location_name, location_data.address, - location_data.default_item, - location_data.tags + location_data.default_item ) region.locations.append(location) 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/options.py b/worlds/pokemon_emerald/options.py index e05b5d96ac74..8fcc74d1c34a 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -123,6 +123,8 @@ class Dexsanity(Toggle): Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need. Each pokedex entry adds a Poke Ball, Great Ball, or Ultra Ball to the pool. + + Warning: This adds a lot of locations and will slow you down significantly. """ display_name = "Dexsanity" @@ -132,6 +134,8 @@ class Trainersanity(Toggle): Defeating a trainer gives you an item. Trainers are no longer missable. Trainers no longer give you money for winning. Each trainer adds a valuable item (Nugget, Stardust, etc.) to the pool. + + Warning: This adds a lot of locations and will slow you down significantly. """ display_name = "Trainersanity" @@ -265,6 +269,8 @@ class RandomizeWildPokemon(Choice): """ Randomizes wild pokemon encounters (grass, caves, water, fishing). + Warning: Matching both base stats and type may severely limit the variety for certain pokemon. + - Vanilla: Wild encounters are unchanged - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst - Match Type: Wild pokemon are replaced with species that share a type with the original @@ -327,6 +333,8 @@ class RandomizeTrainerParties(Choice): """ Randomizes the parties of all trainers. + Warning: Matching both base stats and type may severely limit the variety for certain pokemon. + - Vanilla: Parties are unchanged - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst - Match Type: Trainer pokemon are replaced with species that share a type with the original @@ -357,6 +365,10 @@ class TrainerPartyBlacklist(OptionSet): class ForceFullyEvolved(Range): """ When an opponent uses a pokemon of the specified level or higher, restricts the species to only fully evolved pokemon. + + Only applies when trainer parties are randomized. + + Warning: Combining a low value with matched base stats may severely limit the variety for certain pokemon. """ display_name = "Force Fully Evolved" range_start = 1 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 968a103ccd25..e2a7a4800bfb 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -73,6 +73,7 @@ "MUS_OBTAIN_SYMBOL": 318, "MUS_REGISTER_MATCH_CALL": 135, } +_EVOLUTION_FANFARE_INDEX = list(_FANFARES.keys()).index("MUS_EVOLVED") CAVE_EVENT_NAME_TO_ID = { "TERRA_CAVE_ROUTE_114_1": 1, @@ -114,6 +115,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: + FORTREE_MOVE_TUTOR_INDEX = 24 + if easter_egg[0] == 2: for i in range(30): patch.write_token( @@ -840,18 +860,26 @@ def _randomize_move_tutor_moves(world: "PokemonEmeraldWorld", patch: PokemonEmer # Always set Fortree move tutor to Dig patch.write_token( APTokenTypes.WRITE, - data.rom_addresses["gTutorMoves"] + (24 * 2), + data.rom_addresses["gTutorMoves"] + (FORTREE_MOVE_TUTOR_INDEX * 2), struct.pack("=50%) compatibility + if world.options.tm_tutor_compatibility.value < 50: + compatibility &= ~(1 << FORTREE_MOVE_TUTOR_INDEX) + if world.random.random() < 0.5: + compatibility |= 1 << FORTREE_MOVE_TUTOR_INDEX + patch.write_token( APTokenTypes.WRITE, data.rom_addresses["sTutorLearnsets"] + (species.species_id * 4), - 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("Badge", 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 +42,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 +62,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 +237,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("Badge", 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("Badge", world.player, world.options.norman_count.value) ) else: set_rule( @@ -299,15 +301,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 +349,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 +377,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 +388,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"), @@ -414,13 +416,16 @@ def get_location(location: str): ) # Dewford Town + entrance = get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH") set_rule( - get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH"), + entrance, lambda state: state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player) and state.has("EVENT_TALK_TO_MR_STONE", world.player) and state.has("EVENT_DELIVER_LETTER", world.player) ) + world.multiworld.register_indirect_condition( + get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance) set_rule( get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"), lambda state: @@ -439,7 +444,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 @@ -449,14 +454,17 @@ def get_location(location: str): ) # Route 109 + entrance = get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN") set_rule( - get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN"), + entrance, lambda state: state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player) and state.can_reach("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH", "Entrance", world.player) and state.has("EVENT_TALK_TO_MR_STONE", world.player) and state.has("EVENT_DELIVER_LETTER", world.player) ) + world.multiworld.register_indirect_condition( + get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance) set_rule( get_entrance("REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"), hm_rules["HM03 Surf"] @@ -558,6 +566,10 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) ) + set_rule( + get_location("NPC_GIFT_RECEIVED_COIN_CASE"), + lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) + ) # Route 117 set_rule( @@ -639,15 +651,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 @@ -695,11 +707,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"), @@ -715,11 +727,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"), @@ -808,15 +820,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 @@ -826,11 +838,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"), @@ -846,7 +858,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( @@ -923,11 +935,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"), @@ -1111,17 +1123,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 @@ -1253,17 +1265,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 @@ -1370,17 +1382,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 @@ -1501,7 +1513,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("Badge", world.player, world.options.elite_four_count.value) ) else: set_rule( @@ -1638,10 +1650,6 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) and state.has("EVENT_TURN_OFF_GENERATOR", world.player) ) - set_rule( - get_location("NPC_GIFT_RECEIVED_COIN_CASE"), - lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) - ) # Fallarbor Town set_rule( @@ -1658,7 +1666,8 @@ def get_location(location: str): # Add Itemfinder requirement to hidden items if world.options.require_itemfinder: for location in world.multiworld.get_locations(world.player): - if location.tags is not None and "HiddenItem" in location.tags: + assert isinstance(location, PokemonEmeraldLocation) + if location.key is not None and data.locations[location.key].category == LocationCategory.HIDDEN_ITEM: add_rule( location, lambda state: state.has("Itemfinder", world.player) diff --git a/worlds/pokemon_emerald/sanity_check.py b/worlds/pokemon_emerald/sanity_check.py index 24eb768bfbc5..048b19b46919 100644 --- a/worlds/pokemon_emerald/sanity_check.py +++ b/worlds/pokemon_emerald/sanity_check.py @@ -5,8 +5,6 @@ import logging from typing import List -from .data import load_json_data, data - _IGNORABLE_LOCATIONS = frozenset({ "HIDDEN_ITEM_TRICK_HOUSE_NUGGET", # Is permanently mssiable and has special behavior that sets the flag early @@ -247,12 +245,29 @@ }) +def validate_group_maps() -> bool: + from .data import data + from .groups import _LOCATION_GROUP_MAPS + + failed = False + + for group_name, map_set in _LOCATION_GROUP_MAPS.items(): + for map_name in map_set: + if map_name not in data.maps: + failed = True + logging.error("Pokemon Emerald: Map named %s in location group %s does not exist", map_name, group_name) + + return not failed + + def validate_regions() -> bool: """ Verifies that Emerald's data doesn't have duplicate or missing regions/warps/locations. Meant to catch problems during development like forgetting to add a new location or incorrectly splitting a region. """ + from .data import load_json_data, data + extracted_data_json = load_json_data("extracted_data.json") error_messages: List[str] = [] warn_messages: List[str] = [] diff --git a/worlds/pokemon_emerald/test/__init__.py b/worlds/pokemon_emerald/test/__init__.py index 84ce64003d57..bf2a8da5b0c5 100644 --- a/worlds/pokemon_emerald/test/__init__.py +++ b/worlds/pokemon_emerald/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class PokemonEmeraldTestBase(WorldTestBase): diff --git a/worlds/pokemon_emerald/test/test_warps.py b/worlds/pokemon_emerald/test/test_warps.py index 75a2417dfbe6..d1b5b01dcf7f 100644 --- a/worlds/pokemon_emerald/test/test_warps.py +++ b/worlds/pokemon_emerald/test/test_warps.py @@ -1,4 +1,4 @@ -from test.TestBase import TestBase +from test.bases import TestBase from ..data import Warp diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 5f527033289a..809179cbef74 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.multiworld.random.shuffle(self.dexsanity_table) + 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.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) @@ -296,9 +336,9 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations if attempt > 1: for mon in poke_data.pokemon_data.keys(): state.collect(self.create_item(mon), True) - state.sweep_for_events() - self.multiworld.random.shuffle(badges) - self.multiworld.random.shuffle(badgelocs) + state.sweep_for_advancements() + 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] == "locations": + 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) @@ -486,33 +526,42 @@ def stage_post_fill(cls, multiworld): # This cuts down on time spent calculating the spoiler playthrough. found_mons = set() for sphere in multiworld.get_spheres(): + mon_locations_in_sphere = {} for location in sphere: - if (location.game == "Pokemon Red and Blue" and (location.item.name in poke_data.pokemon_data.keys() - or "Static " in location.item.name) + if (location.game == location.item.game == "Pokemon Red and Blue" + and (location.item.name in poke_data.pokemon_data.keys() or "Static " in location.item.name) and location.item.advancement): key = (location.player, location.item.name) if key in found_mons: location.item.classification = ItemClassification.useful else: - found_mons.add(key) + mon_locations_in_sphere.setdefault(key, []).append(location) + for key, mon_locations in mon_locations_in_sphere.items(): + found_mons.add(key) + if len(mon_locations) > 1: + # Sort for deterministic results. + mon_locations.sort() + # Convert all but the first to useful classification. + for location in mon_locations[1:]: + location.item.classification = ItemClassification.useful 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 +577,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 +597,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 +620,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 +665,60 @@ 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, + "v5_update": True, } + 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/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md index 6499c9501263..67024c5b52ec 100644 --- a/worlds/pokemon_rb/docs/setup_es.md +++ b/worlds/pokemon_rb/docs/setup_es.md @@ -11,8 +11,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux - Instrucciones de instalaciÃŗn detalladas para BizHawk se pueden encontrar en el enlace de arriba. - Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que tambiÊn se encuentra en el enlace de arriba. -- El cliente incorporado de Archipelago, que se puede encontrar [aquí](https://github.com/ArchipelagoMW/Archipelago/releases) - (selecciona `Pokemon Client` durante la instalaciÃŗn). +- El cliente incorporado de Archipelago, que se puede encontrar [aquí](https://github.com/ArchipelagoMW/Archipelago/releases). - Los ROMs originales de PokÊmon Red y/o Blue. La comunidad de Archipelago no puede proveerlos. ## Software Opcional @@ -75,27 +74,41 @@ Y los siguientes caracteres especiales (cada uno ocupa un carÃĄcter): ## Unirse a un juego MultiWorld -### Obtener tu parche de PokÊmon +### Generar y parchar un juego -Cuando te unes a un juego multiworld, se te pedirÃĄ que entregues tu archivo YAML a quien lo estÊ organizando. -Una vez que la generaciÃŗn acabe, el anfitriÃŗn te darÃĄ un enlace a tu archivo, o un .zip con los archivos de -todos. Tu archivo tiene una extensiÃŗn `.apred` o `.apblue`. +1. Crea tu archivo de opciones (YAML). +2. Sigue las instrucciones generales de Archipelago para [generar un juego](../../Archipelago/setup/en#generating-a-game). +Haciendo esto se generarÃĄ un archivo de salida. Tu parche tendrÃĄ la extensiÃŗn de archivo `.apred` o `.apblue`. +3. Abre `ArchipelagoLauncher.exe` +4. Selecciona "Open Patch" en el lado izquierdo y selecciona tu parche. +5. Si es tu primera vez parchando, se te pedirÃĄ que selecciones tu ROM original. +6. Un archivo `.gb` parchado serÃĄ creado en el mismo lugar donde estÃĄ el parche. +7. La primera vez que abras un parche con BizHawk Client, tambiÊn se te pedira ubicar `EmuHawk.exe` en tu +instalaciÃŗn de BizHawk. -Haz doble clic en tu archivo `.apred` o `.apblue` para que se ejecute el cliente y realice el parcheado del ROM. -Una vez acabe ese proceso (esto puede tardar un poco), el cliente y el emulador se abrirÃĄn automÃĄticamente (si es que se -ha asociado la extensiÃŗn al emulador tal como fue recomendado) +Si estÃĄs jugando una semilla single-player y no te importa tener seguimiento ni pistas, puedes terminar aqui, cerrar el +cliente, y cargar el ROM parchado en cualquier emulador. Sin embargo, para multiworlds y otras funciones de Archipelago, +continÃēa con los pasos abajo, usando el emulador BizHawk. ### Conectarse al multiserver -Una vez ejecutado tanto el cliente como el emulador, hay que conectarlos. Abre la carpeta de instalaciÃŗn de Archipelago, -luego abre `data/lua`, y simplemente arrastra el archivo `connector_pkmn_rb.lua` a la ventana principal de Emuhawk. -(Alternativamente, puedes abrir la consola de Lua manualmente. En Emuhawk ir a Tools > Lua Console, luego ir al menÃē -`Script` âŒĒ `Open Script`, navegar a la ubicaciÃŗn de `connector_pkmn_rb.lua` y seleccionarlo.) - -Para conectar el cliente con el servidor, simplemente pon `:` en la caja de texto superior y presiona -enter (si el servidor tiene contraseÃąa, en la caja de texto inferior escribir `/connect : [contraseÃąa]`) - -Ahora ya estÃĄs listo para tu aventura en Kanto. +Por defecto, abrir un parche harÃĄ los pasos del 1 al 5 automÃĄticamente. Incluso asi, es bueno memorizarlos en caso de +que tengas que cerrar y volver a abrir el juego por alguna razÃŗn. + +1. PokÊmon Red/Blue usa el BizHawk Client de Archipelago. Si el cliente no estÃĄ abierto desde cuando parchaste tu juego, +puedes volverlo a abrir desde el Launcher. +2. AsegÃērate que EmuHawk esta cargando el ROM parchado. +3. En EmuHawk, ir a `Tools > Lua Console`. Esta ventana debe quedarse abierta mientras se juega. +4. En la ventana de Lua Console, ir a `Script > Open Scriptâ€Ļ`. +5. Navegar a tu carpeta de instalaciÃŗn de Archipelago y abrir `data/lua/connector_bizhawk_generic.lua`. +6. El emulador se puede congelar por unos segundos hasta que logre conectarse al cliente. Esto es normal. La ventana del +BizHawk Client debería indicar que se logro conectar y reconocer PokÊmon Red/Blue. +7. Para conectar el cliente al servidor, ingresa la direcciÃŗn y el puerto (por ejemplo, `archipelago.gg:38281`) en el +campo de texto superior del cliente y y haz clic en Connect. + +Para conectar el cliente al multiserver simplemente escribe `:` en el campo de texto superior y +presiona enter (si el servidor usa contraseÃąa, escribe en el campo de texto inferior +`/connect :[contraseÃąa]`) ## Auto-Tracking diff --git a/worlds/pokemon_rb/encounters.py b/worlds/pokemon_rb/encounters.py index 6d1762b0ca71..aa20114787c3 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,22 +170,26 @@ def process_pokemon_locations(self): encounter_slots = encounter_slots_master.copy() zone_mapping = {} - if self.multiworld.randomize_wild_pokemon[self.player]: + zone_placed_mons = {} + + 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) zone = " - ".join(location.name.split(" - ")[:-1]) if zone not in zone_mapping: zone_mapping[zone] = {} + if zone not in zone_placed_mons: + zone_placed_mons[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) + mon = randomize_pokemon(self, original_mon, [m for m in mons_list if m not in zone_placed_mons[zone]], + 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,38 +198,39 @@ 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 location.item.location = location locations.append(location) zone_mapping[zone][original_mon] = mon + zone_placed_mons[zone].append(mon) 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 +238,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 +251,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] @@ -270,4 +275,4 @@ def process_pokemon_locations(self): location.item = self.create_item(slot.original_item) location.locked = True location.item.location = location - placed_mons[location.item.name] += 1 \ No newline at end of file + placed_mons[location.item.name] += 1 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 251beb59cc18..467139c39e94 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: @@ -221,7 +223,7 @@ def __init__(self, flag): Missable(92)), LocationData("Victory Road 2F-C", "East Item", "Full Heal", rom_addresses["Missable_Victory_Road_2F_Item_2"], Missable(93)), - LocationData("Victory Road 2F-W", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"], + LocationData("Victory Road 2F-C", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"], Missable(94)), LocationData("Victory Road 2F-NW", "North Item Near Moltres", "Guard Spec", rom_addresses["Missable_Victory_Road_2F_Item_4"], Missable(95)), @@ -395,11 +397,11 @@ 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), - LocationData("Seafoam Islands B2F-NW", "Hidden Item Rock", "Nugget", rom_addresses['Hidden_Item_Seafoam_Islands_B2F'], Hidden(25), inclusion=hidden_items), + LocationData("Seafoam Islands B2F-SW", "Hidden Item Rock", "Nugget", rom_addresses['Hidden_Item_Seafoam_Islands_B2F'], Hidden(25), inclusion=hidden_items), LocationData("Seafoam Islands B4F-W", "Hidden Item Corner Island", "Ultra Ball", rom_addresses['Hidden_Item_Seafoam_Islands_B4F'], Hidden(26), inclusion=hidden_items), LocationData("Pokemon Mansion 1F", "Hidden Item Block Near Entrance Carpet", "Moon Stone", rom_addresses['Hidden_Item_Pokemon_Mansion_1F'], Hidden(27), inclusion=hidden_moon_stones), LocationData("Pokemon Mansion 3F-SW", "Hidden Item Behind Burglar", "Max Revive", rom_addresses['Hidden_Item_Pokemon_Mansion_3F'], Hidden(28), inclusion=hidden_items), @@ -427,7 +429,7 @@ def __init__(self, flag): LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50), inclusion=hidden_items), LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51), inclusion=hidden_items), LocationData("Cerulean City-Badge House Backyard", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items), - LocationData("Route 4-E", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), + LocationData("Route 4-C", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), LocationData("Oak's Lab", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)), @@ -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 bd6515913aca..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 +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 locations, 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 } @@ -418,10 +426,10 @@ class ExpModifier(NamedRange): """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16.""" display_name = "Exp Modifier" default = 16 - range_start = default / 4 + range_start = default // 4 range_end = 255 special_range_names = { - "half": default / 2, + "half": default // 2, "normal": default, "double": default * 2, "triple": default * 3, @@ -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,103 +878,105 @@ class RandomizePokemonPalettes(Choice): option_completely_random = 3 -pokemon_rb_options = { - "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 -} \ No newline at end of file +@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 a9206fe66753..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", @@ -2439,7 +2447,7 @@ def adds_reachable_entrances(item): state_copy = state.copy() state_copy.collect(item, True) - state.sweep_for_events(locations=event_locations) + state.sweep_for_advancements(locations=event_locations) new_reachable_entrances = len([entrance for entrance in entrances if entrance in reachable_entrances or entrance.parent_region.can_reach(state_copy)]) return new_reachable_entrances > len(reachable_entrances) @@ -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 @@ -2480,12 +2488,12 @@ def dead_end(e): while entrances: state.update_reachable_regions(player) - state.sweep_for_events(locations=event_locations) + 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 21dceb75e8df..3c1cdc57e99b 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] != "locations": - 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 @@ -102,102 +94,105 @@ def prize_rule(i): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), + "Victory Road 1F - Top Item": lambda state: logic.can_strength(state, world, player), + "Victory Road 1F - Left Item": lambda state: logic.can_strength(state, world, 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 +276,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/raft/Options.py b/worlds/raft/Options.py index 696d4dbab477..efe460b50353 100644 --- a/worlds/raft/Options.py +++ b/worlds/raft/Options.py @@ -1,4 +1,5 @@ -from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink +from dataclasses import dataclass +from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink, PerGameCommonOptions class MinimumResourcePackAmount(Range): """The minimum amount of resources available in a resource pack""" @@ -47,6 +48,8 @@ class IslandFrequencyLocations(Choice): option_progressive = 4 option_anywhere = 5 default = 2 + def is_filling_frequencies_in_world(self): + return self.value <= self.option_random_on_island_random_order class IslandGenerationDistance(Choice): """Sets how far away islands spawn from you when you input their coordinates into the Receiver.""" @@ -76,16 +79,16 @@ class PaddleboardMode(Toggle): """Sets later story islands to be in logic without an Engine or Steering Wheel. May require lots of paddling.""" display_name = "Paddleboard Mode" -raft_options = { - "minimum_resource_pack_amount": MinimumResourcePackAmount, - "maximum_resource_pack_amount": MaximumResourcePackAmount, - "duplicate_items": DuplicateItems, - "filler_item_types": FillerItemTypes, - "island_frequency_locations": IslandFrequencyLocations, - "island_generation_distance": IslandGenerationDistance, - "expensive_research": ExpensiveResearch, - "progressive_items": ProgressiveItems, - "big_island_early_crafting": BigIslandEarlyCrafting, - "paddleboard_mode": PaddleboardMode, - "death_link": DeathLink -} +@dataclass +class RaftOptions(PerGameCommonOptions): + minimum_resource_pack_amount: MinimumResourcePackAmount + maximum_resource_pack_amount: MaximumResourcePackAmount + duplicate_items: DuplicateItems + filler_item_types: FillerItemTypes + island_frequency_locations: IslandFrequencyLocations + island_generation_distance: IslandGenerationDistance + expensive_research: ExpensiveResearch + progressive_items: ProgressiveItems + big_island_early_crafting: BigIslandEarlyCrafting + paddleboard_mode: PaddleboardMode + death_link: DeathLink diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index e84068a6f584..b6bd49c187cd 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -5,10 +5,10 @@ class RaftLogic(LogicMixin): def raft_paddleboard_mode_enabled(self, player): - return self.multiworld.paddleboard_mode[player].value + return bool(self.multiworld.worlds[player].options.paddleboard_mode) def raft_big_islands_available(self, player): - return self.multiworld.big_island_early_crafting[player].value or self.raft_can_access_radio_tower(player) + return bool(self.multiworld.worlds[player].options.big_island_early_crafting) or self.raft_can_access_radio_tower(player) def raft_can_smelt_items(self, player): return self.has("Smelter", player) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index e96cd4471268..3e33b417c04b 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -6,7 +6,7 @@ from .Regions import create_regions, getConnectionName from .Rules import set_rules -from .Options import raft_options +from .Options import RaftOptions from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, Tutorial from ..AutoWorld import World, WebWorld @@ -37,16 +37,17 @@ class RaftWorld(World): lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values())) location_name_to_id = locations_lookup_name_to_id - option_definitions = raft_options + options_dataclass = RaftOptions + options: RaftOptions required_client_version = (0, 3, 4) def create_items(self): - minRPSpecified = self.multiworld.minimum_resource_pack_amount[self.player].value - maxRPSpecified = self.multiworld.maximum_resource_pack_amount[self.player].value + minRPSpecified = self.options.minimum_resource_pack_amount.value + maxRPSpecified = self.options.maximum_resource_pack_amount.value minimumResourcePackAmount = min(minRPSpecified, maxRPSpecified) maximumResourcePackAmount = max(minRPSpecified, maxRPSpecified) - isFillingFrequencies = self.multiworld.island_frequency_locations[self.player].value <= 3 + isFillingFrequencies = self.options.island_frequency_locations.is_filling_frequencies_in_world() # Generate item pool pool = [] frequencyItems = [] @@ -56,28 +57,24 @@ def create_items(self): frequencyItems.append(raft_item) else: pool.append(raft_item) - if isFillingFrequencies: - if not hasattr(self.multiworld, "raft_frequencyItemsPerPlayer"): - self.multiworld.raft_frequencyItemsPerPlayer = {} - self.multiworld.raft_frequencyItemsPerPlayer[self.player] = frequencyItems extraItemNamePool = [] extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot if extras > 0: - if (self.multiworld.filler_item_types[self.player].value != 1): # Use resource packs + if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs for packItem in resourcePackItems: for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1): extraItemNamePool.append(createResourcePackName(i, packItem)) - if self.multiworld.filler_item_types[self.player].value != 0: # Use duplicate items + if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items dupeItemPool = item_table.copy() # Remove frequencies if necessary - if self.multiworld.island_frequency_locations[self.player].value != 5: # Not completely random locations + if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations # If we let frequencies stay in with progressive-frequencies, the progressive-frequency item # will be included 7 times. This is a massive flood of progressive-frequency items, so we # instead add progressive-frequency as its own item a smaller amount of times to prevent # flooding the duplicate item pool with them. - if self.multiworld.island_frequency_locations[self.player].value == 4: + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive: for _ in range(2): # Progressives are not in item_pool, need to create faux item for duplicate item pool # This can still be filtered out later by duplicate_items setting @@ -86,9 +83,9 @@ def create_items(self): dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"]) # Remove progression or non-progression items if necessary - if (self.multiworld.duplicate_items[self.player].value == 0): # Progression only + if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True) - elif (self.multiworld.duplicate_items[self.player].value == 1): # Non-progression only + elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False) dupeItemPool = list(dupeItemPool) @@ -108,21 +105,19 @@ def create_items(self): self.multiworld.get_location("Utopia Complete", self.player).place_locked_item( RaftItem("Victory", ItemClassification.progression, None, player=self.player)) + if frequencyItems: + self.place_frequencyItems(frequencyItems) + def set_rules(self): set_rules(self.multiworld, self.player) def create_regions(self): create_regions(self.multiworld, self.player) - def get_pre_fill_items(self): - if self.multiworld.island_frequency_locations[self.player] in [0, 1, 2, 3]: - return [loc.item for loc in self.multiworld.get_filled_locations()] - return [] - def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name - shouldUseProgressive = ((isFrequency and self.multiworld.island_frequency_locations[self.player].value == 4) - or (not isFrequency and self.multiworld.progressive_items[self.player].value)) + shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive) + or (not isFrequency and self.options.progressive_items)) if shouldUseProgressive and name in progressive_table: name = progressive_table[name] return self.create_item(name) @@ -151,24 +146,38 @@ def collect_item(self, state, item, remove=False): return super(RaftWorld, self).collect_item(state, item, remove) - def pre_fill(self): - if self.multiworld.island_frequency_locations[self.player] == 0: # Vanilla - self.setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency") - self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency") - self.setLocationItem("Relay Station quest", "Caravan Island Frequency") - self.setLocationItem("Caravan Island Frequency to Tangaroa", "Tangaroa Frequency") - self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency") - self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency") - self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] == 1: # Random on island - self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency") - self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency") - self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency") - self.setLocationItemFromRegion("CaravanIsland", "Tangaroa Frequency") - self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") - self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency") - self.setLocationItemFromRegion("Temperance", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] in [2, 3]: + def place_frequencyItems(self, frequencyItems): + def setLocationItem(location: str, itemName: str): + itemToUse = next(filter(lambda itm: itm.name == itemName, frequencyItems)) + frequencyItems.remove(itemToUse) + self.get_location(location).place_locked_item(itemToUse) + + def setLocationItemFromRegion(region: str, itemName: str): + itemToUse = next(filter(lambda itm: itm.name == itemName, frequencyItems)) + frequencyItems.remove(itemToUse) + location = self.random.choice(list(loc for loc in location_table if loc["region"] == region)) + self.get_location(location["name"]).place_locked_item(itemToUse) + + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_vanilla: + setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency") + setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency") + setLocationItem("Relay Station quest", "Caravan Island Frequency") + setLocationItem("Caravan Island Frequency to Tangaroa", "Tangaroa Frequency") + setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency") + setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency") + setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency") + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island: + setLocationItemFromRegion("RadioTower", "Vasagatan Frequency") + setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency") + setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency") + setLocationItemFromRegion("CaravanIsland", "Tangaroa Frequency") + setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") + setLocationItemFromRegion("Varuna Point", "Temperance Frequency") + setLocationItemFromRegion("Temperance", "Utopia Frequency") + elif self.options.island_frequency_locations in [ + self.options.island_frequency_locations.option_random_island_order, + self.options.island_frequency_locations.option_random_on_island_random_order + ]: locationToFrequencyItemMap = { "Vasagatan": "Vasagatan Frequency", "BalboaIsland": "Balboa Island Frequency", @@ -196,28 +205,17 @@ def pre_fill(self): else: currentLocation = availableLocationList[0] # Utopia (only one left in list) availableLocationList.remove(currentLocation) - if self.multiworld.island_frequency_locations[self.player] == 2: # Random island order - self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) - elif self.multiworld.island_frequency_locations[self.player] == 3: # Random on island random order - self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_island_order: + setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island_random_order: + setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) previousLocation = currentLocation - def setLocationItem(self, location: str, itemName: str): - itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player])) - self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse) - self.multiworld.get_location(location, self.player).place_locked_item(itemToUse) - - def setLocationItemFromRegion(self, region: str, itemName: str): - itemToUse = next(filter(lambda itm: itm.name == itemName, self.multiworld.raft_frequencyItemsPerPlayer[self.player])) - self.multiworld.raft_frequencyItemsPerPlayer[self.player].remove(itemToUse) - location = self.random.choice(list(loc for loc in location_table if loc["region"] == region)) - self.multiworld.get_location(location["name"], self.player).place_locked_item(itemToUse) - def fill_slot_data(self): return { - "IslandGenerationDistance": self.multiworld.island_generation_distance[self.player].value, - "ExpensiveResearch": bool(self.multiworld.expensive_research[self.player].value), - "DeathLink": bool(self.multiworld.death_link[self.player].value) + "IslandGenerationDistance": self.options.island_generation_distance.value, + "ExpensiveResearch": bool(self.options.expensive_research), + "DeathLink": bool(self.options.death_link) } def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): 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..7ffdd459db48 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,32 @@ 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): # 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 +68,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 +188,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 +199,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 +211,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 +219,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 +227,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 +235,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/ror2/rules.py b/worlds/ror2/rules.py index 2e6b018f42fb..f0ab9f28313f 100644 --- a/worlds/ror2/rules.py +++ b/worlds/ror2/rules.py @@ -31,23 +31,17 @@ def has_all_items(multiworld: MultiWorld, items: Set[str], region: str, player: # Checks to see if chest/shrine are accessible def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\ -> None: - if item_number == 1: - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) + location_name = f"{environment}: {item_type} {item_number}" + if item_type == "Scavenger": # scavengers need to be locked till after a full loop since that is when they are capable of spawning. # (While technically the requirement is just beating 5 stages, this will ensure that the player will have # a long enough run to have enough director credits for scavengers and # help prevent being stuck in the same stages until that point). - if item_type == "Scavenger": - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) and state.has("Stage 5", player) + multiworld.get_location(location_name, player).access_rule = \ + lambda state: state.has(environment, player) and state.has("Stage 5", player) else: - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: check_location(state, environment, player, item_number, item_type) - - -def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool: - return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player) + multiworld.get_location(location_name, player).access_rule = \ + lambda state: state.has(environment, player) def set_rules(ror2_world: "RiskOfRainWorld") -> None: diff --git a/worlds/saving_princess/Client.py b/worlds/saving_princess/Client.py new file mode 100644 index 000000000000..29a97bb667c0 --- /dev/null +++ b/worlds/saving_princess/Client.py @@ -0,0 +1,258 @@ +import argparse +import zipfile +from io import BytesIO + +import bsdiff4 +from datetime import datetime +import hashlib +import json +import logging +import os +import requests +import secrets +import shutil +import subprocess +from tkinter import messagebox +from typing import Any, Dict, Set +import urllib +import urllib.parse + +import Utils +from .Constants import * +from . import SavingPrincessWorld + +files_to_clean: Set[str] = { + "D3DX9_43.dll", + "data.win", + "m_boss.ogg", + "m_brainos.ogg", + "m_coldarea.ogg", + "m_escape.ogg", + "m_hotarea.ogg", + "m_hsis_dark.ogg", + "m_hsis_power.ogg", + "m_introarea.ogg", + "m_malakhov.ogg", + "m_miniboss.ogg", + "m_ninja.ogg", + "m_purple.ogg", + "m_space_idle.ogg", + "m_stonearea.ogg", + "m_swamp.ogg", + "m_zzz.ogg", + "options.ini", + "Saving Princess v0_8.exe", + "splash.png", + "gm-apclientpp.dll", + "LICENSE", + "original_data.win", + "versions.json", +} + +file_hashes: Dict[str, str] = { + "D3DX9_43.dll": "86e39e9161c3d930d93822f1563c280d", + "Saving Princess v0_8.exe": "cc3ad10c782e115d93c5b9fbc5675eaf", + "original_data.win": "f97b80204bd9ae535faa5a8d1e5eb6ca", +} + + +class UrlResponse: + def __init__(self, response_code: int, data: Any): + self.response_code = response_code + self.data = data + + +def get_date(target_asset: str) -> str: + """Provided the name of an asset, fetches its update date""" + try: + with open("versions.json", "r") as versions_json: + return json.load(versions_json)[target_asset] + except (FileNotFoundError, KeyError, json.decoder.JSONDecodeError): + return "2000-01-01T00:00:00Z" # arbitrary old date + + +def set_date(target_asset: str, date: str) -> None: + """Provided the name of an asset and a date, sets it update date""" + try: + with open("versions.json", "r") as versions_json: + versions = json.load(versions_json) + versions[target_asset] = date + except (FileNotFoundError, KeyError, json.decoder.JSONDecodeError): + versions = {target_asset: date} + with open("versions.json", "w") as versions_json: + json.dump(versions, versions_json) + + +def get_timestamp(date: str) -> float: + """Parses a GitHub REST API date into a timestamp""" + return datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ").timestamp() + + +def send_request(request_url: str) -> UrlResponse: + """Fetches status code and json response from given url""" + response = requests.get(request_url) + if response.status_code == 200: # success + try: + data = response.json() + except requests.exceptions.JSONDecodeError: + raise RuntimeError(f"Unable to fetch data. (status code {response.status_code}).") + else: + data = {} + return UrlResponse(response.status_code, data) + + +def update(target_asset: str, url: str) -> bool: + """ + Returns True if the data was fetched and installed + (or it was already on the latest version, or the user refused the update) + Returns False if rate limit was exceeded + """ + try: + logging.info(f"Checking for {target_asset} updates.") + response = send_request(url) + if response.response_code == 403: # rate limit exceeded + return False + assets = response.data[0]["assets"] + for asset in assets: + if target_asset in asset["name"]: + newest_date: str = asset["updated_at"] + release_url: str = asset["browser_download_url"] + break + else: + raise RuntimeError(f"Failed to locate {target_asset} amongst the assets.") + except (KeyError, IndexError, TypeError, RuntimeError): + update_error = f"Failed to fetch latest {target_asset}." + messagebox.showerror("Failure", update_error) + raise RuntimeError(update_error) + try: + update_available = get_timestamp(newest_date) > get_timestamp(get_date(target_asset)) + if update_available and messagebox.askyesnocancel(f"New {target_asset}", + "Would you like to install the new version now?"): + # unzip and patch + with urllib.request.urlopen(release_url) as download: + with zipfile.ZipFile(BytesIO(download.read())) as zf: + zf.extractall() + patch_game() + set_date(target_asset, newest_date) + except (ValueError, RuntimeError, urllib.error.HTTPError): + update_error = f"Failed to apply update." + messagebox.showerror("Failure", update_error) + raise RuntimeError(update_error) + return True + + +def patch_game() -> None: + """Applies the patch to data.win""" + logging.info("Proceeding to patch.") + with open(PATCH_NAME, "rb") as patch: + with open("original_data.win", "rb") as data: + patched_data = bsdiff4.patch(data.read(), patch.read()) + with open("data.win", "wb") as data: + data.write(patched_data) + logging.info("Done!") + + +def is_install_valid() -> bool: + """Checks that the mandatory files that we cannot replace do exist in the current folder""" + for file_name, expected_hash in file_hashes.items(): + if not os.path.exists(file_name): + return False + with open(file_name, "rb") as clean: + current_hash = hashlib.md5(clean.read()).hexdigest() + if not secrets.compare_digest(current_hash, expected_hash): + return False + return True + + +def install() -> None: + """Extracts all the game files into the mod installation folder""" + logging.info("Mod installation missing or corrupted, proceeding to reinstall.") + # get the cab file and extract it into the installation folder + with open(SavingPrincessWorld.settings.exe_path, "rb") as exe: + # find the cab header + logging.info("Looking for cab archive inside exe.") + cab_found: bool = False + while not cab_found: + cab_found = exe.read(1) == b'M' and exe.read(1) == b'S' and exe.read(1) == b'C' and exe.read(1) == b'F' + exe.read(4) # skip reserved1, always 0 + cab_size: int = int.from_bytes(exe.read(4), "little") # read size in bytes + exe.seek(-12, 1) # move the cursor back to the start of the cab file + logging.info(f"Archive found at offset {hex(exe.seek(0, 1))}, size: {hex(cab_size)}.") + logging.info("Extracting cab archive from exe.") + with open("saving_princess.cab", "wb") as cab: + cab.write(exe.read(cab_size)) + + # clean up files from previous installations + for file_name in files_to_clean: + if os.path.exists(file_name): + os.remove(file_name) + + logging.info("Extracting files from cab archive.") + if Utils.is_windows: + subprocess.run(["Extrac32", "/Y", "/E", "saving_princess.cab"]) + else: + if shutil.which("wine") is not None: + subprocess.run(["wine", "Extrac32", "/Y", "/E", "saving_princess.cab"]) + elif shutil.which("7z") is not None: + subprocess.run(["7z", "e", "saving_princess.cab"]) + else: + error = "Could not find neither wine nor 7z.\n\nPlease install either the wine or the p7zip package." + messagebox.showerror("Missing package!", f"Error: {error}") + raise RuntimeError(error) + os.remove("saving_princess.cab") # delete the cab file + + shutil.copyfile("data.win", "original_data.win") # and make a copy of data.win + logging.info("Done!") + + +def launch(*args: str) -> Any: + """Check args, then the mod installation, then launch the game""" + name: str = "" + password: str = "" + server: str = "" + if args: + parser = argparse.ArgumentParser(description=f"{GAME_NAME} Client Launcher") + parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") + 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) + if url.scheme == "archipelago": + server = f'--server="{url.hostname}:{url.port}"' + if url.username: + name = f'--name="{urllib.parse.unquote(url.username)}"' + if url.password: + password = f'--password="{urllib.parse.unquote(url.password)}"' + else: + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") + + Utils.init_logging(CLIENT_NAME, exception_logger="Client") + + os.chdir(SavingPrincessWorld.settings.install_folder) + + # check that the mod installation is valid + if not is_install_valid(): + if messagebox.askyesnocancel(f"Mod installation missing or corrupted!", + "Would you like to reinstall now?"): + install() + # if there is no mod installation, and we are not installing it, then there isn't much to do + else: + return + + # check for updates + if not update(DOWNLOAD_NAME, DOWNLOAD_URL): + messagebox.showinfo("Rate limit exceeded", + "GitHub REST API limit exceeded, could not check for updates.\n\n" + "This will not prevent the game from being played if it was already playable.") + + # and try to launch the game + if SavingPrincessWorld.settings.launch_game: + logging.info("Launching game.") + try: + subprocess.Popen(f"{SavingPrincessWorld.settings.launch_command} {name} {password} {server}") + except FileNotFoundError: + error = ("Could not run the game!\n\n" + "Please check that launch_command in options.yaml or host.yaml is set up correctly.") + messagebox.showerror("Command error!", f"Error: {error}") + raise RuntimeError(error) diff --git a/worlds/saving_princess/Constants.py b/worlds/saving_princess/Constants.py new file mode 100644 index 000000000000..0dde18779727 --- /dev/null +++ b/worlds/saving_princess/Constants.py @@ -0,0 +1,97 @@ +GAME_NAME: str = "Saving Princess" +BASE_ID: int = 0x53565052494E # SVPRIN + +# client installation data +CLIENT_NAME = f"{GAME_NAME.replace(' ', '')}Client" +GAME_HASH = "35a111d0149fae1f04b7b3fea42c5319" +PATCH_NAME = "saving_princess_basepatch.bsdiff4" +DOWNLOAD_NAME = "saving_princess_archipelago.zip" +DOWNLOAD_URL = "https://api.github.com/repos/LeonarthCG/saving-princess-archipelago/releases" + +# item names +ITEM_WEAPON_CHARGE: str = "Powered Blaster" +ITEM_WEAPON_FIRE: str = "Flamethrower" +ITEM_WEAPON_ICE: str = "Ice Spreadshot" +ITEM_WEAPON_VOLT: str = "Volt Laser" +ITEM_MAX_HEALTH: str = "Life Extension" +ITEM_MAX_AMMO: str = "Clip Extension" +ITEM_RELOAD_SPEED: str = "Faster Reload" +ITEM_SPECIAL_AMMO: str = "Special Extension" +ITEM_JACKET: str = "Jacket" + +EP_ITEM_GUARD_GONE: str = "Cave Key" +EP_ITEM_CLIFF_GONE: str = "Volcanic Key" +EP_ITEM_ACE_GONE: str = "Arctic Key" +EP_ITEM_SNAKE_GONE: str = "Swamp Key" +EP_ITEM_POWER_ON: str = "System Power" + +FILLER_ITEM_HEAL: str = "Full Heal" +FILLER_ITEM_QUICK_FIRE: str = "Quick-fire Mode" +FILLER_ITEM_ACTIVE_CAMO: str = "Active Camouflage" + +TRAP_ITEM_ICE: str = "Ice Trap" +TRAP_ITEM_SHAKES: str = "Shake Trap" +TRAP_ITEM_NINJA: str = "Ninja Trap" + +EVENT_ITEM_GUARD_GONE: str = "Guard neutralized" +EVENT_ITEM_CLIFF_GONE: str = "Cliff neutralized" +EVENT_ITEM_ACE_GONE: str = "Ace neutralized" +EVENT_ITEM_SNAKE_GONE: str = "Snake neutralized" +EVENT_ITEM_POWER_ON: str = "Power restored" +EVENT_ITEM_VICTORY: str = "PRINCESS" + +# location names, EP stands for Expanded Pool +LOCATION_CAVE_AMMO: str = "Cave: After Wallboss" +LOCATION_CAVE_RELOAD: str = "Cave: Balcony" +LOCATION_CAVE_HEALTH: str = "Cave: Spike pit" +LOCATION_CAVE_WEAPON: str = "Cave: Powered Blaster chest" +LOCATION_VOLCANIC_RELOAD: str = "Volcanic: Hot coals" +LOCATION_VOLCANIC_HEALTH: str = "Volcanic: Under bridge" +LOCATION_VOLCANIC_AMMO: str = "Volcanic: Behind wall" +LOCATION_VOLCANIC_WEAPON: str = "Volcanic: Flamethrower chest" +LOCATION_ARCTIC_AMMO: str = "Arctic: Before pipes" +LOCATION_ARCTIC_RELOAD: str = "Arctic: After Guard" +LOCATION_ARCTIC_HEALTH: str = "Arctic: Under snow" +LOCATION_ARCTIC_WEAPON: str = "Arctic: Ice Spreadshot chest" +LOCATION_JACKET: str = "Arctic: Jacket chest" +LOCATION_HUB_AMMO: str = "Hub: Hidden near Arctic" +LOCATION_HUB_HEALTH: str = "Hub: Hidden near Cave" +LOCATION_HUB_RELOAD: str = "Hub: Hidden near Swamp" +LOCATION_SWAMP_AMMO: str = "Swamp: Bramble room" +LOCATION_SWAMP_HEALTH: str = "Swamp: Down the chimney" +LOCATION_SWAMP_RELOAD: str = "Swamp: Wall maze" +LOCATION_SWAMP_SPECIAL: str = "Swamp: Special Extension chest" +LOCATION_ELECTRICAL_RELOAD: str = "Electrical: Near generator" +LOCATION_ELECTRICAL_HEALTH: str = "Electrical: Behind wall" +LOCATION_ELECTRICAL_AMMO: str = "Electrical: Before Malakhov" +LOCATION_ELECTRICAL_WEAPON: str = "Electrical: Volt Laser chest" + +EP_LOCATION_CAVE_MINIBOSS: str = "Cave: Wallboss (Boss)" +EP_LOCATION_CAVE_BOSS: str = "Cave: Guard (Boss)" +EP_LOCATION_VOLCANIC_BOSS: str = "Volcanic: Cliff (Boss)" +EP_LOCATION_ARCTIC_BOSS: str = "Arctic: Ace (Boss)" +EP_LOCATION_HUB_CONSOLE: str = "Hub: Console login" +EP_LOCATION_HUB_NINJA_SCARE: str = "Hub: Ninja scare (Boss?)" +EP_LOCATION_SWAMP_BOSS: str = "Swamp: Snake (Boss)" +EP_LOCATION_ELEVATOR_NINJA_FIGHT: str = "Elevator: Ninja (Boss)" +EP_LOCATION_ELECTRICAL_EXTRA: str = "Electrical: Tesla orb" +EP_LOCATION_ELECTRICAL_MINIBOSS: str = "Electrical: Generator (Boss)" +EP_LOCATION_ELECTRICAL_BOSS: str = "Electrical: Malakhov (Boss)" +EP_LOCATION_ELECTRICAL_FINAL_BOSS: str = "Electrical: BRAINOS (Boss)" + +EVENT_LOCATION_GUARD_GONE: str = "Cave status" +EVENT_LOCATION_CLIFF_GONE: str = "Volcanic status" +EVENT_LOCATION_ACE_GONE: str = "Arctic status" +EVENT_LOCATION_SNAKE_GONE: str = "Swamp status" +EVENT_LOCATION_POWER_ON: str = "Generator status" +EVENT_LOCATION_VICTORY: str = "Mission objective" + +# region names +REGION_MENU: str = "Menu" +REGION_CAVE: str = "Cave" +REGION_VOLCANIC: str = "Volcanic" +REGION_ARCTIC: str = "Arctic" +REGION_HUB: str = "Hub" +REGION_SWAMP: str = "Swamp" +REGION_ELECTRICAL: str = "Electrical" +REGION_ELECTRICAL_POWERED: str = "Electrical (Power On)" diff --git a/worlds/saving_princess/Items.py b/worlds/saving_princess/Items.py new file mode 100644 index 000000000000..4c1fe78a9c72 --- /dev/null +++ b/worlds/saving_princess/Items.py @@ -0,0 +1,98 @@ +from typing import Optional, Dict, Tuple + +from BaseClasses import Item, ItemClassification as ItemClass + +from .Constants import * + + +class SavingPrincessItem(Item): + game: str = GAME_NAME + + +class ItemData: + item_class: ItemClass + code: Optional[int] + count: int # Number of copies for the item that will be made of class item_class + count_extra: int # Number of extra copies for the item that will be made as useful + + def __init__(self, item_class: ItemClass, code: Optional[int] = None, count: int = 1, count_extra: int = 0): + self.item_class = item_class + + self.code = code + if code is not None: + self.code += BASE_ID + + # if this is filler, a trap or an event, ignore the count + if self.item_class == ItemClass.filler or self.item_class == ItemClass.trap or code is None: + self.count = 0 + self.count_extra = 0 + else: + self.count = count + self.count_extra = count_extra + + def create_item(self, player: int): + return SavingPrincessItem(item_data_names[self], self.item_class, self.code, player) + + +item_dict_weapons: Dict[str, ItemData] = { + ITEM_WEAPON_CHARGE: ItemData(ItemClass.progression, 0), + ITEM_WEAPON_FIRE: ItemData(ItemClass.progression, 1), + ITEM_WEAPON_ICE: ItemData(ItemClass.progression, 2), + ITEM_WEAPON_VOLT: ItemData(ItemClass.progression, 3), +} + +item_dict_upgrades: Dict[str, ItemData] = { + ITEM_MAX_HEALTH: ItemData(ItemClass.progression, 4, 2, 4), + ITEM_MAX_AMMO: ItemData(ItemClass.progression, 5, 2, 4), + ITEM_RELOAD_SPEED: ItemData(ItemClass.progression, 6, 4, 2), + ITEM_SPECIAL_AMMO: ItemData(ItemClass.useful, 7), +} + +item_dict_base: Dict[str, ItemData] = { + **item_dict_weapons, + **item_dict_upgrades, + ITEM_JACKET: ItemData(ItemClass.useful, 8), +} + +item_dict_keys: Dict[str, ItemData] = { + EP_ITEM_GUARD_GONE: ItemData(ItemClass.progression, 9), + EP_ITEM_CLIFF_GONE: ItemData(ItemClass.progression, 10), + EP_ITEM_ACE_GONE: ItemData(ItemClass.progression, 11), + EP_ITEM_SNAKE_GONE: ItemData(ItemClass.progression, 12), +} + +item_dict_expanded: Dict[str, ItemData] = { + **item_dict_base, + **item_dict_keys, + EP_ITEM_POWER_ON: ItemData(ItemClass.progression, 13), +} + +item_dict_filler: Dict[str, ItemData] = { + FILLER_ITEM_HEAL: ItemData(ItemClass.filler, 14), + FILLER_ITEM_QUICK_FIRE: ItemData(ItemClass.filler, 15), + FILLER_ITEM_ACTIVE_CAMO: ItemData(ItemClass.filler, 16), +} + +item_dict_traps: Dict[str, ItemData] = { + TRAP_ITEM_ICE: ItemData(ItemClass.trap, 17), + TRAP_ITEM_SHAKES: ItemData(ItemClass.trap, 18), + TRAP_ITEM_NINJA: ItemData(ItemClass.trap, 19), +} + +item_dict_events: Dict[str, ItemData] = { + EVENT_ITEM_GUARD_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_CLIFF_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_ACE_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_SNAKE_GONE: ItemData(ItemClass.progression), + EVENT_ITEM_POWER_ON: ItemData(ItemClass.progression), + EVENT_ITEM_VICTORY: ItemData(ItemClass.progression), +} + +item_dict: Dict[str, ItemData] = { + **item_dict_expanded, + **item_dict_filler, + **item_dict_traps, + **item_dict_events, +} + +item_data_names: Dict[ItemData, str] = {value: key for key, value in item_dict.items()} diff --git a/worlds/saving_princess/Locations.py b/worlds/saving_princess/Locations.py new file mode 100644 index 000000000000..bc7b0f0d6efd --- /dev/null +++ b/worlds/saving_princess/Locations.py @@ -0,0 +1,82 @@ +from typing import Optional, Dict + +from BaseClasses import Location + +from .Constants import * + + +class SavingPrincessLocation(Location): + game: str = GAME_NAME + + +class LocData: + code: Optional[int] + + def __init__(self, code: Optional[int] = None): + if code is not None: + self.code = code + BASE_ID + else: + self.code = None + + +location_dict_base: Dict[str, LocData] = { + LOCATION_CAVE_AMMO: LocData(0), + LOCATION_CAVE_RELOAD: LocData(1), + LOCATION_CAVE_HEALTH: LocData(2), + LOCATION_CAVE_WEAPON: LocData(3), + LOCATION_VOLCANIC_RELOAD: LocData(4), + LOCATION_VOLCANIC_HEALTH: LocData(5), + LOCATION_VOLCANIC_AMMO: LocData(6), + LOCATION_VOLCANIC_WEAPON: LocData(7), + LOCATION_ARCTIC_AMMO: LocData(8), + LOCATION_ARCTIC_RELOAD: LocData(9), + LOCATION_ARCTIC_HEALTH: LocData(10), + LOCATION_ARCTIC_WEAPON: LocData(11), + LOCATION_JACKET: LocData(12), + LOCATION_HUB_AMMO: LocData(13), + LOCATION_HUB_HEALTH: LocData(14), + LOCATION_HUB_RELOAD: LocData(15), + LOCATION_SWAMP_AMMO: LocData(16), + LOCATION_SWAMP_HEALTH: LocData(17), + LOCATION_SWAMP_RELOAD: LocData(18), + LOCATION_SWAMP_SPECIAL: LocData(19), + LOCATION_ELECTRICAL_RELOAD: LocData(20), + LOCATION_ELECTRICAL_HEALTH: LocData(21), + LOCATION_ELECTRICAL_AMMO: LocData(22), + LOCATION_ELECTRICAL_WEAPON: LocData(23), +} + +location_dict_expanded: Dict[str, LocData] = { + **location_dict_base, + EP_LOCATION_CAVE_MINIBOSS: LocData(24), + EP_LOCATION_CAVE_BOSS: LocData(25), + EP_LOCATION_VOLCANIC_BOSS: LocData(26), + EP_LOCATION_ARCTIC_BOSS: LocData(27), + EP_LOCATION_HUB_CONSOLE: LocData(28), + EP_LOCATION_HUB_NINJA_SCARE: LocData(29), + EP_LOCATION_SWAMP_BOSS: LocData(30), + EP_LOCATION_ELEVATOR_NINJA_FIGHT: LocData(31), + EP_LOCATION_ELECTRICAL_EXTRA: LocData(32), + EP_LOCATION_ELECTRICAL_MINIBOSS: LocData(33), + EP_LOCATION_ELECTRICAL_BOSS: LocData(34), + EP_LOCATION_ELECTRICAL_FINAL_BOSS: LocData(35), +} + +location_dict_event_expanded: Dict[str, LocData] = { + EVENT_LOCATION_VICTORY: LocData(), +} + +# most event locations are only relevant without expanded pool +location_dict_events: Dict[str, LocData] = { + EVENT_LOCATION_GUARD_GONE: LocData(), + EVENT_LOCATION_CLIFF_GONE: LocData(), + EVENT_LOCATION_ACE_GONE: LocData(), + EVENT_LOCATION_SNAKE_GONE: LocData(), + EVENT_LOCATION_POWER_ON: LocData(), + **location_dict_event_expanded, +} + +location_dict: Dict[str, LocData] = { + **location_dict_expanded, + **location_dict_events, +} diff --git a/worlds/saving_princess/Options.py b/worlds/saving_princess/Options.py new file mode 100644 index 000000000000..75135a1d15bb --- /dev/null +++ b/worlds/saving_princess/Options.py @@ -0,0 +1,183 @@ +from dataclasses import dataclass +from typing import Dict, Any + +from Options import PerGameCommonOptions, DeathLink, StartInventoryPool, Choice, DefaultOnToggle, Range, Toggle, \ + OptionGroup + + +class ExpandedPool(DefaultOnToggle): + """ + Determines if places other than chests and special weapons will be locations. + This includes boss fights as well as powering the tesla orb and completing the console login. + In Expanded Pool, system power is instead restored when receiving the System Power item. + Similarly, the final area door will open once the four Key items, one for each main area, have been found. + """ + display_name = "Expanded Item Pool" + + +class InstantSaving(DefaultOnToggle): + """ + When enabled, save points activate with no delay when touched. + This makes saving much faster, at the cost of being unable to pick and choose when to save in order to save warp. + """ + display_name = "Instant Saving" + + +class SprintAvailability(Choice): + """ + Determines under which conditions the debug sprint is made accessible to the player. + To sprint, hold down Ctrl if playing on keyboard, or Left Bumper if on gamepad (remappable). + With Jacket: you will not be able to sprint until after the Jacket item has been found. + """ + display_name = "Sprint Availability" + option_never_available = 0 + option_always_available = 1 + option_available_with_jacket = 2 + default = option_available_with_jacket + + +class CliffWeaponUpgrade(Choice): + """ + Determines which weapon Cliff uses against you, base or upgraded. + This does not change the available strategies all that much. + Vanilla: Cliff adds fire to his grenades if Ace has been defeated. + If playing with the expanded pool, the Arctic Key will trigger the change instead. + """ + display_name = "Cliff Weapon Upgrade" + option_never_upgraded = 0 + option_always_upgraded = 1 + option_vanilla = 2 + default = option_always_upgraded + + +class AceWeaponUpgrade(Choice): + """ + Determines which weapon Ace uses against you, base or upgraded. + Ace with his base weapon is very hard to dodge, the upgraded weapon offers a more balanced experience. + Vanilla: Ace uses ice attacks if Cliff has been defeated. + If playing with the expanded pool, the Volcanic Key will trigger the change instead. + """ + display_name = "Ace Weapon Upgrade" + option_never_upgraded = 0 + option_always_upgraded = 1 + option_vanilla = 2 + default = option_always_upgraded + + +class ScreenShakeIntensity(Range): + """ + Percentage multiplier for screen shake effects. + 0% means the screen will not shake at all. + 100% means the screen shake will be the same as in vanilla. + """ + display_name = "Screen Shake Intensity %" + range_start = 0 + range_end = 100 + default = 50 + + +class IFramesDuration(Range): + """ + Percentage multiplier for Portia's invincibility frames. + 0% means you will have no invincibility frames. + 100% means invincibility frames will be the same as vanilla. + """ + display_name = "IFrame Duration %" + range_start = 0 + range_end = 400 + default = 100 + + +class TrapChance(Range): + """ + Likelihood of a filler item becoming a trap. + """ + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 50 + + +class MusicShuffle(Toggle): + """ + Enables music shuffling. + The title screen song is not shuffled, as it plays before the client connects. + """ + display_name = "Music Shuffle" + + +@dataclass +class SavingPrincessOptions(PerGameCommonOptions): + # generation options + start_inventory_from_pool: StartInventoryPool + expanded_pool: ExpandedPool + trap_chance: TrapChance + # gameplay options + death_link: DeathLink + instant_saving: InstantSaving + sprint_availability: SprintAvailability + cliff_weapon_upgrade: CliffWeaponUpgrade + ace_weapon_upgrade: AceWeaponUpgrade + iframes_duration: IFramesDuration + # aesthetic options + shake_intensity: ScreenShakeIntensity + music_shuffle: MusicShuffle + + +groups = [ + OptionGroup("Generation Options", [ + ExpandedPool, + TrapChance, + ]), + OptionGroup("Gameplay Options", [ + DeathLink, + InstantSaving, + SprintAvailability, + CliffWeaponUpgrade, + AceWeaponUpgrade, + IFramesDuration, + ]), + OptionGroup("Aesthetic Options", [ + ScreenShakeIntensity, + MusicShuffle, + ]), +] + +presets = { + "Vanilla-like": { + "expanded_pool": False, + "trap_chance": 0, + "death_link": False, + "instant_saving": False, + "sprint_availability": SprintAvailability.option_never_available, + "cliff_weapon_upgrade": CliffWeaponUpgrade.option_vanilla, + "ace_weapon_upgrade": AceWeaponUpgrade.option_vanilla, + "iframes_duration": 100, + "shake_intensity": 100, + "music_shuffle": False, + }, + "Easy": { + "expanded_pool": True, + "trap_chance": 0, + "death_link": False, + "instant_saving": True, + "sprint_availability": SprintAvailability.option_always_available, + "cliff_weapon_upgrade": CliffWeaponUpgrade.option_never_upgraded, + "ace_weapon_upgrade": AceWeaponUpgrade.option_always_upgraded, + "iframes_duration": 200, + "shake_intensity": 50, + "music_shuffle": False, + }, + "Hard": { + "expanded_pool": True, + "trap_chance": 100, + "death_link": True, + "instant_saving": True, + "sprint_availability": SprintAvailability.option_never_available, + "cliff_weapon_upgrade": CliffWeaponUpgrade.option_always_upgraded, + "ace_weapon_upgrade": AceWeaponUpgrade.option_never_upgraded, + "iframes_duration": 50, + "shake_intensity": 100, + "music_shuffle": False, + } +} diff --git a/worlds/saving_princess/Regions.py b/worlds/saving_princess/Regions.py new file mode 100644 index 000000000000..b67bda9b2784 --- /dev/null +++ b/worlds/saving_princess/Regions.py @@ -0,0 +1,110 @@ +from typing import List, Dict + +from BaseClasses import MultiWorld, Region, Entrance + +from . import Locations +from .Constants import * + + +region_dict: Dict[str, List[str]] = { + REGION_MENU: [], + REGION_CAVE: [ + LOCATION_CAVE_AMMO, + LOCATION_CAVE_RELOAD, + LOCATION_CAVE_HEALTH, + LOCATION_CAVE_WEAPON, + EP_LOCATION_CAVE_MINIBOSS, + EP_LOCATION_CAVE_BOSS, + EVENT_LOCATION_GUARD_GONE, + ], + REGION_VOLCANIC: [ + LOCATION_VOLCANIC_RELOAD, + LOCATION_VOLCANIC_HEALTH, + LOCATION_VOLCANIC_AMMO, + LOCATION_VOLCANIC_WEAPON, + EP_LOCATION_VOLCANIC_BOSS, + EVENT_LOCATION_CLIFF_GONE, + ], + REGION_ARCTIC: [ + LOCATION_ARCTIC_AMMO, + LOCATION_ARCTIC_RELOAD, + LOCATION_ARCTIC_HEALTH, + LOCATION_ARCTIC_WEAPON, + LOCATION_JACKET, + EP_LOCATION_ARCTIC_BOSS, + EVENT_LOCATION_ACE_GONE, + ], + REGION_HUB: [ + LOCATION_HUB_AMMO, + LOCATION_HUB_HEALTH, + LOCATION_HUB_RELOAD, + EP_LOCATION_HUB_CONSOLE, + EP_LOCATION_HUB_NINJA_SCARE, + ], + REGION_SWAMP: [ + LOCATION_SWAMP_AMMO, + LOCATION_SWAMP_HEALTH, + LOCATION_SWAMP_RELOAD, + LOCATION_SWAMP_SPECIAL, + EP_LOCATION_SWAMP_BOSS, + EVENT_LOCATION_SNAKE_GONE, + ], + REGION_ELECTRICAL: [ + EP_LOCATION_ELEVATOR_NINJA_FIGHT, + LOCATION_ELECTRICAL_WEAPON, + EP_LOCATION_ELECTRICAL_MINIBOSS, + EP_LOCATION_ELECTRICAL_EXTRA, + EVENT_LOCATION_POWER_ON, + ], + REGION_ELECTRICAL_POWERED: [ + LOCATION_ELECTRICAL_RELOAD, + LOCATION_ELECTRICAL_HEALTH, + LOCATION_ELECTRICAL_AMMO, + EP_LOCATION_ELECTRICAL_BOSS, + EP_LOCATION_ELECTRICAL_FINAL_BOSS, + EVENT_LOCATION_VICTORY, + ], +} + + +def set_region_locations(region: Region, location_names: List[str], is_pool_expanded: bool): + location_pool = {**Locations.location_dict_base, **Locations.location_dict_events} + if is_pool_expanded: + location_pool = {**Locations.location_dict_expanded, **Locations.location_dict_event_expanded} + region.locations = [ + Locations.SavingPrincessLocation( + region.player, + name, + Locations.location_dict[name].code, + region + ) for name in location_names if name in location_pool.keys() + ] + + +def create_regions(multiworld: MultiWorld, player: int, is_pool_expanded: bool): + for region_name, location_names in region_dict.items(): + region = Region(region_name, player, multiworld) + set_region_locations(region, location_names, is_pool_expanded) + multiworld.regions.append(region) + connect_regions(multiworld, player) + + +def connect_regions(multiworld: MultiWorld, player: int): + # and add a connection from the menu to the hub region + menu = multiworld.get_region(REGION_MENU, player) + hub = multiworld.get_region(REGION_HUB, player) + connection = Entrance(player, f"{REGION_HUB} entrance", menu) + menu.exits.append(connection) + connection.connect(hub) + + # now add an entrance from every other region to hub + for region_name in [REGION_CAVE, REGION_VOLCANIC, REGION_ARCTIC, REGION_SWAMP, REGION_ELECTRICAL]: + connection = Entrance(player, f"{region_name} entrance", hub) + hub.exits.append(connection) + connection.connect(multiworld.get_region(region_name, player)) + + # and finally, the connection between the final region and its powered version + electrical = multiworld.get_region(REGION_ELECTRICAL, player) + connection = Entrance(player, f"{REGION_ELECTRICAL_POWERED} entrance", electrical) + electrical.exits.append(connection) + connection.connect(multiworld.get_region(REGION_ELECTRICAL_POWERED, player)) diff --git a/worlds/saving_princess/Rules.py b/worlds/saving_princess/Rules.py new file mode 100644 index 000000000000..3ee8a4f2c433 --- /dev/null +++ b/worlds/saving_princess/Rules.py @@ -0,0 +1,132 @@ +from typing import TYPE_CHECKING +from BaseClasses import CollectionState, Location, Entrance +from worlds.generic.Rules import set_rule +from .Constants import * +if TYPE_CHECKING: + from . import SavingPrincessWorld + + +def set_rules(world: "SavingPrincessWorld"): + def get_location(name: str) -> Location: + return world.get_location(name) + + def get_region_entrance(name: str) -> Entrance: + return world.get_entrance(f"{name} entrance") + + def can_hover(state: CollectionState) -> bool: + # portia can hover if she has a weapon other than the powered blaster and 4 reload speed upgrades + return ( + state.has(ITEM_RELOAD_SPEED, world.player, 4) + and state.has_any({ITEM_WEAPON_FIRE, ITEM_WEAPON_ICE, ITEM_WEAPON_VOLT}, world.player) + ) + + # guarantees that the player will have some upgrades before having to face the area bosses, except for cave + def nice_check(state: CollectionState) -> bool: + return ( + state.has(ITEM_MAX_HEALTH, world.player) + and state.has(ITEM_MAX_AMMO, world.player) + and state.has(ITEM_RELOAD_SPEED, world.player, 2) + ) + + # same as above, but for the final area + def super_nice_check(state: CollectionState) -> bool: + return ( + state.has(ITEM_MAX_HEALTH, world.player, 2) + and state.has(ITEM_MAX_AMMO, world.player, 2) + and state.has(ITEM_RELOAD_SPEED, world.player, 4) + and state.has(ITEM_WEAPON_CHARGE, world.player) + # at least one special weapon, other than powered blaster + and state.has_any({ITEM_WEAPON_FIRE, ITEM_WEAPON_ICE, ITEM_WEAPON_VOLT}, world.player) + ) + + # all special weapons required so that the boss' weapons can be targeted + def all_weapons(state: CollectionState) -> bool: + return state.has_all({ITEM_WEAPON_FIRE, ITEM_WEAPON_ICE, ITEM_WEAPON_VOLT}, world.player) + + def is_gate_unlocked(state: CollectionState) -> bool: + # the gate unlocks with all 4 boss keys, although this only applies to extended pool + if world.is_pool_expanded: + # in expanded, the final area requires all the boss keys + return ( + state.has_all( + {EP_ITEM_GUARD_GONE, EP_ITEM_CLIFF_GONE, EP_ITEM_ACE_GONE, EP_ITEM_SNAKE_GONE}, + world.player + ) and super_nice_check(state) + ) + else: + # in base pool, check that the main area bosses can be defeated + return state.has_all( + {EVENT_ITEM_GUARD_GONE, EVENT_ITEM_CLIFF_GONE, EVENT_ITEM_ACE_GONE, EVENT_ITEM_SNAKE_GONE}, + world.player + ) and super_nice_check(state) + + def is_power_on(state: CollectionState) -> bool: + # in expanded pool, the power item is what determines this, else it happens when the generator is powered + return state.has(EP_ITEM_POWER_ON if world.is_pool_expanded else EVENT_ITEM_POWER_ON, world.player) + + # set the location rules + # this is behind the blast door to arctic + set_rule(get_location(LOCATION_HUB_AMMO), lambda state: state.has(ITEM_WEAPON_CHARGE, world.player)) + # these are behind frozen doors + for location_name in [LOCATION_ARCTIC_HEALTH, LOCATION_JACKET]: + set_rule(get_location(location_name), lambda state: state.has(ITEM_WEAPON_FIRE, world.player)) + # these would require damage boosting otherwise + set_rule(get_location(LOCATION_VOLCANIC_RELOAD), + lambda state: state.has(ITEM_WEAPON_ICE, world.player) or can_hover(state)) + set_rule(get_location(LOCATION_SWAMP_AMMO), lambda state: can_hover(state)) + if world.is_pool_expanded: + # does not spawn until the guard has been defeated + set_rule(get_location(EP_LOCATION_HUB_NINJA_SCARE), lambda state: state.has(EP_ITEM_GUARD_GONE, world.player)) + # generator cannot be turned on without the volt laser + set_rule( + get_location(EP_LOCATION_ELECTRICAL_EXTRA if world.is_pool_expanded else EVENT_LOCATION_POWER_ON), + lambda state: state.has(ITEM_WEAPON_VOLT, world.player) + ) + # the roller is not very intuitive to get past without 4 ammo + set_rule(get_location(LOCATION_CAVE_WEAPON), lambda state: state.has(ITEM_MAX_AMMO, world.player)) + set_rule( + get_location(EP_LOCATION_CAVE_BOSS if world.is_pool_expanded else EVENT_LOCATION_GUARD_GONE), + lambda state: state.has(ITEM_MAX_AMMO, world.player) + ) + + # guarantee some upgrades to be found before bosses + boss_locations = [LOCATION_VOLCANIC_WEAPON, LOCATION_ARCTIC_WEAPON, LOCATION_SWAMP_SPECIAL] + if world.is_pool_expanded: + boss_locations += [EP_LOCATION_VOLCANIC_BOSS, EP_LOCATION_ARCTIC_BOSS, EP_LOCATION_SWAMP_BOSS] + else: + boss_locations += [EVENT_LOCATION_CLIFF_GONE, EVENT_LOCATION_ACE_GONE, EVENT_LOCATION_SNAKE_GONE] + for location_name in boss_locations: + set_rule(get_location(location_name), lambda state: nice_check(state)) + + # set the basic access rules for the regions, these are all behind blast doors + for region_name in [REGION_VOLCANIC, REGION_ARCTIC, REGION_SWAMP]: + set_rule(get_region_entrance(region_name), lambda state: state.has(ITEM_WEAPON_CHARGE, world.player)) + + # now for the final area regions, which have different rules based on if ep is on + set_rule(get_region_entrance(REGION_ELECTRICAL), lambda state: is_gate_unlocked(state)) + set_rule(get_region_entrance(REGION_ELECTRICAL_POWERED), lambda state: is_power_on(state)) + + # brainos requires all weapons, cannot destroy the cannons otherwise + if world.is_pool_expanded: + set_rule(get_location(EP_LOCATION_ELECTRICAL_FINAL_BOSS), lambda state: all_weapons(state)) + # and we need to beat brainos to beat the game + set_rule(get_location(EVENT_LOCATION_VICTORY), lambda state: all_weapons(state)) + + # if not expanded pool, place the events for the boss kills and generator + if not world.is_pool_expanded: + # accessible with no items + cave_item = world.create_item(EVENT_ITEM_GUARD_GONE) + get_location(EVENT_LOCATION_GUARD_GONE).place_locked_item(cave_item) + volcanic_item = world.create_item(EVENT_ITEM_CLIFF_GONE) + get_location(EVENT_LOCATION_CLIFF_GONE).place_locked_item(volcanic_item) + arctic_item = world.create_item(EVENT_ITEM_ACE_GONE) + get_location(EVENT_LOCATION_ACE_GONE).place_locked_item(arctic_item) + swamp_item = world.create_item(EVENT_ITEM_SNAKE_GONE) + get_location(EVENT_LOCATION_SNAKE_GONE).place_locked_item(swamp_item) + power_item = world.create_item(EVENT_ITEM_POWER_ON) + get_location(EVENT_LOCATION_POWER_ON).place_locked_item(power_item) + + # and, finally, set the victory event + victory_item = world.create_item(EVENT_ITEM_VICTORY) + get_location(EVENT_LOCATION_VICTORY).place_locked_item(victory_item) + world.multiworld.completion_condition[world.player] = lambda state: state.has(EVENT_ITEM_VICTORY, world.player) diff --git a/worlds/saving_princess/__init__.py b/worlds/saving_princess/__init__.py new file mode 100644 index 000000000000..4109f356fd2e --- /dev/null +++ b/worlds/saving_princess/__init__.py @@ -0,0 +1,174 @@ +from typing import ClassVar, Dict, Any, Type, List, Union + +import Utils +from BaseClasses import Tutorial, ItemClassification as ItemClass +from Options import PerGameCommonOptions, OptionError +from settings import Group, UserFilePath, LocalFolderPath, Bool +from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import components, Component, launch_subprocess, Type as ComponentType +from . import Options, Items, Locations +from .Constants import * + + +def launch_client(*args: str): + from .Client import launch + launch_subprocess(launch(*args), name=CLIENT_NAME) + + +components.append( + Component(f"{GAME_NAME} Client", game_name=GAME_NAME, func=launch_client, component_type=ComponentType.CLIENT, supports_uri=True) +) + + +class SavingPrincessSettings(Group): + class GamePath(UserFilePath): + """Path to the game executable from which files are extracted""" + description = "the Saving Princess game executable" + is_exe = True + md5s = [GAME_HASH] + + class InstallFolder(LocalFolderPath): + """Path to the mod installation folder""" + description = "the folder to install Saving Princess Archipelago to" + + class LaunchGame(Bool): + """Set this to false to never autostart the game""" + + class LaunchCommand(str): + """ + The console command that will be used to launch the game + The command will be executed with the installation folder as the current directory + """ + + exe_path: GamePath = GamePath("Saving Princess.exe") + install_folder: InstallFolder = InstallFolder("Saving Princess") + launch_game: Union[LaunchGame, bool] = True + launch_command: LaunchCommand = LaunchCommand('"Saving Princess v0_8.exe"' if Utils.is_windows + else 'wine "Saving Princess v0_8.exe"') + + +class SavingPrincessWeb(WebWorld): + theme = "partyTime" + bug_report_page = "https://github.com/LeonarthCG/saving-princess-archipelago/issues" + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Saving Princess for Archipelago multiworld.", + "English", + "setup_en.md", + "setup/en", + ["LeonarthCG"] + ) + tutorials = [setup_en] + options_presets = Options.presets + option_groups = Options.groups + + +class SavingPrincessWorld(World): + """ + Explore a space station crawling with rogue machines and even rival bounty hunters + with the same objective as you - but with far, far different intentions! + + Expand your arsenal as you collect upgrades to your trusty arm cannon and armor! + """ # Excerpt from itch + game = GAME_NAME + web = SavingPrincessWeb() + required_client_version = (0, 5, 0) + + topology_present = False + + item_name_to_id = { + key: value.code for key, value in (Items.item_dict.items() - Items.item_dict_events.items()) + } + location_name_to_id = { + key: value.code for key, value in (Locations.location_dict.items() - Locations.location_dict_events.items()) + } + + item_name_groups = { + "Weapons": {key for key in Items.item_dict_weapons.keys()}, + "Upgrades": {key for key in Items.item_dict_upgrades.keys()}, + "Keys": {key for key in Items.item_dict_keys.keys()}, + "Filler": {key for key in Items.item_dict_filler.keys()}, + "Traps": {key for key in Items.item_dict_traps.keys()}, + } + + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = Options.SavingPrincessOptions + options: Options.SavingPrincessOptions + settings_key = "saving_princess_settings" + settings: ClassVar[SavingPrincessSettings] + + is_pool_expanded: bool = False + music_table: List[int] = list(range(16)) + + def generate_early(self) -> None: + if not self.player_name.isascii(): + raise OptionError(f"{self.player_name}'s name must be only ASCII.") + self.is_pool_expanded = self.options.expanded_pool > 0 + if self.options.music_shuffle: + self.random.shuffle(self.music_table) + # find zzz and purple and swap them back to their original positions + for song_id in [9, 13]: + song_index = self.music_table.index(song_id) + t = self.music_table[song_id] + self.music_table[song_id] = song_id + self.music_table[song_index] = t + + def create_regions(self) -> None: + from .Regions import create_regions + create_regions(self.multiworld, self.player, self.is_pool_expanded) + + def create_items(self) -> None: + items_made: int = 0 + + # now, for each item + item_dict = Items.item_dict_expanded if self.is_pool_expanded else Items.item_dict_base + for item_name, item_data in item_dict.items(): + # create count copies of the item + for i in range(item_data.count): + self.multiworld.itempool.append(self.create_item(item_name)) + items_made += item_data.count + # and create count_extra useful copies of the item + original_item_class: ItemClass = item_data.item_class + item_data.item_class = ItemClass.useful + for i in range(item_data.count_extra): + self.multiworld.itempool.append(self.create_item(item_name)) + item_data.item_class = original_item_class + items_made += item_data.count_extra + + # get the number of unfilled locations, that is, locations for items - items generated + location_count = len(Locations.location_dict_base) + if self.is_pool_expanded: + location_count = len(Locations.location_dict_expanded) + junk_count: int = location_count - items_made + + # and generate as many junk items as unfilled locations + for i in range(junk_count): + self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) + + def create_item(self, name: str) -> Items.SavingPrincessItem: + return Items.item_dict[name].create_item(self.player) + + def get_filler_item_name(self) -> str: + filler_list = list(Items.item_dict_filler.keys()) + # check if this is going to be a trap + if self.random.randint(0, 99) < self.options.trap_chance: + filler_list = list(Items.item_dict_traps.keys()) + # and return one of the names at random + return self.random.choice(filler_list) + + def set_rules(self): + from .Rules import set_rules + set_rules(self) + + def fill_slot_data(self) -> Dict[str, Any]: + slot_data = self.options.as_dict( + "death_link", + "expanded_pool", + "instant_saving", + "sprint_availability", + "cliff_weapon_upgrade", + "ace_weapon_upgrade", + "shake_intensity", + "iframes_duration", + ) + slot_data["music_table"] = self.music_table + return slot_data diff --git a/worlds/saving_princess/docs/en_Saving Princess.md b/worlds/saving_princess/docs/en_Saving Princess.md new file mode 100644 index 000000000000..3eb6b9831c38 --- /dev/null +++ b/worlds/saving_princess/docs/en_Saving Princess.md @@ -0,0 +1,55 @@ +# Saving Princess + +## Quick Links +- [Setup Guide](/tutorial/Saving%20Princess/setup/en) +- [Options Page](/games/Saving%20Princess/player-options) +- [Saving Princess Archipelago GitHub](https://github.com/LeonarthCG/saving-princess-archipelago) + +## What changes have been made? + +The game has had several changes made to add new features and prevent issues. The most important changes are the following: +- There is an in-game connection settings menu, autotracker and client console. +- New save files are created and used automatically for each seed and slot played. +- The game window can now be dragged and a new integer scaling option has been added. + +## What items and locations get shuffled? + +The chest contents and special weapons are the items and locations that get shuffled. + +Additionally, there are new items to work as filler and traps, ranging from a full health and ammo restore to spawning a Ninja on top of you. + +The Expanded Pool option, which is enabled by default, adds a few more items and locations: +- Completing the intro sequence, powering the generator with the Volt Laser and defeating each boss become locations. +- 4 Keys will be shuffled, which serve to open the door to the final area in place of defeating the main area bosses. +- A System Power item will be shuffled, which restores power to the final area instead of this happening when the generator is powered. + +## What does another world's item look like in Saving Princess? + +Some locations, such as boss kills, have no visual representation, but those that do will have the Archipelago icon. + +Once the item is picked up, a textbox will inform you of the item that was found as well as the player that will be receiving it. + +These textboxes will have colored backgrounds and comments about the item category. +For example, progression items will have a purple background and say "Looks plenty important!". + +## When the player receives an item, what happens? + +When you receive an item, a textbox will show up. +This textbox shows both which item you got and which player sent it to you. + +If you send an item to yourself, however, the sending player will be omitted. + +## Unique Local Commands + +The following commands are only available when using the in-game console in Saving Princess: +- `/help` Returns the help listing. +- `/options` Lists currently applied options. +- `/resync` Manually triggers a resync. This also resends all found locations. +- `/unstuck` Sets save point to the first save point. Portia is then killed. +- `/deathlink [on|off]` Toggles or sets death link mode. +- `/instantsaving [on|off]` Toggles or sets instant saving. +- `/sprint {never|always|jacket}` Sets sprint mode. +- `/cliff {never|always|vanilla}` Sets Cliff's weapon upgrade condition. +- `/ace {never|always|vanilla}` Sets Ace's weapon upgrade condition. +- `/iframes n` Sets the iframe duration % multiplier to n, where 0 <= n <= 400. +- `/shake n` Sets the shake intensity % multiplier to n, where 0 <= n <= 100. diff --git a/worlds/saving_princess/docs/setup_en.md b/worlds/saving_princess/docs/setup_en.md new file mode 100644 index 000000000000..5f7cfb49f560 --- /dev/null +++ b/worlds/saving_princess/docs/setup_en.md @@ -0,0 +1,148 @@ +# Saving Princess Setup Guide + +## Quick Links +- [Game Info](/games/Saving%20Princess/info/en) +- [Options Page](/games/Saving%20Princess/player-options) +- [Saving Princess Archipelago GitHub](https://github.com/LeonarthCG/saving-princess-archipelago) + +## Installation Procedures + +### Automated Installation + +*These instructions have only been tested on Windows and Ubuntu.* + +Once everything is set up, it is recommended to continue launching the game through this method, as it will check for any updates to the mod and automatically apply them. +This is also the method used by the Automatic Connection described further below. + +1. Purchase and download [Saving Princess](https://brainos.itch.io/savingprincess) +2. Download and install the latest [Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases/latest) +3. Launch `ArchipelagoLauncher` and click on "Saving Princess Client" + * You will probably need to scroll down on the Clients column to see it +4. Follow the prompts + * On Linux, you will need one of either Wine or 7z for the automated installation + +When launching the game, Windows machines will simply run the executable. For any other OS, the launcher defaults to trying to run the game through Wine. You can change this by modifying the `launch_command` in `options.yaml` or `host.yaml`, under the `saving_princess_settings` section. + +### Manual Windows Installation + +Required software: +- Saving Princess, found at its [itch.io Store Page](https://brainos.itch.io/savingprincess) +- `saving_princess_basepatch.bsdiff4` and `gm-apclientpp.dll`, from [saving_princess_archipelago.zip](https://github.com/LeonarthCG/saving-princess-archipelago/releases/latest) +- Software that can decompress the previous files, such as [7-zip](https://www.7-zip.org/download.html) +- A way to apply `.bsdiff4` patches, such as [bspatch](https://www.romhacking.net/utilities/929/) + +Steps: +1. Extract all files from `Saving Princess.exe`, as if it were a `.7z` file + * Feel free to rename `Saving Princess.exe` to `Saving Princess.exe.7z` if needed + * If installed through the itch app, you can find the installation directory from the game's page, pressing the cog button, then "Manage" and finally "Open folder in explorer" +2. Extract all files from `saving_princess_archipelago.zip` into the same directory as the files extracted in the previous step + * This should include, at least, `saving_princess_basepatch.bsdiff4` and `gm-apclientpp.dll` +3. If you don't have `original_data.win`, copy `data.win` and rename its copy to `original_data.win` + * By keeping an unmodified copy of `data.win`, you will have an easier time updating in the future +4. Apply the `saving_princess_basepatch.bsdiff4` patch using your patching software +5. To launch the game, run `Saving Princess v0_8.exe` + +### Manual Linux Installation + +*These instructions have only been tested on Ubuntu.* + +The game does run mostly well through Wine, so it is possible to play on Linux, although there are some minor sprite displacement and sound issues from time to time. + +You can follow the instructions for Windows with very few changes: + +* Using the `p7zip-full` package to decompress the file. +``` +7z e 'Saving Princess.exe' +``` +* And the `bsdiff` package for patching. +``` +bspatch original_data.win data.win saving_princess_basepatch.bsdiff4 +``` + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). + +### Where do I get a YAML file? + +You can customize your options by visiting the [Saving Princess Player Options Page](/games/Saving%20Princess/player-options). + +### Verifying your YAML file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/check). + +## 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 "Saving Princess Client" button in the prompt. + * This launches the same client described in the Automated Installation section. +4. Upon reaching the title screen, a connection attempt will automatically be started. + +Note that this updates your Saving Princess saved connection details, which are described in the Manual Connection section. + +### Manual Connection + +After launching the game, enter the Archipelago options menu through the in-game button with the Archipelago icon. +From here, enter the different menus and type in the following details in their respective fields: +- **server:port** (e.g. `archipelago.gg:38281`) + * If hosting on the website, this detail will be shown in your created room. +- **slot name** (e.g. `Player`) + * This is your player name, which you chose along with your player options. +- **password** (e.g. `123456`) + * If the room does not have a password, it can be left empty. + +This configuration persists through launches and even updates. + +With your settings filled, start a connection attempt by pressing on the title screen's "CONNECT!" button. + +Once connected, the button will become one of either "NEW GAME" or "CONTINUE". +The game automatically keeps a save file for each seed and slot combination, so you do not need to manually move or delete save files. + +All that's left is pressing on the button again to start playing. If you are waiting for a countdown, press "NEW GAME" when the countdown finishes. + +## Gameplay Questions + +### Do I need to save the game before I stop playing? + +It is safe to close the game at any point while playing, your progress will be kept. + +### What happens if I lose connection? + +If a disconnection occurs, you will see the HUD connection indicator go grey. +From here, the game will automatically try to reconnect. +You can tell it succeeded if the indicator regains its color. + +If the game is unable to reconnect, save and restart. + +Although you can keep playing while disconnected, you won't get any items until you reconnect, not even items found in your own game. +Once reconnected, however, all of your progress will sync up. + +### I got an item, but it did not say who sent it to me + +Items sent to you by yourself do not list the sender. + +Additionally, if you get an item while already having the max for that item (for example, you have 9 ammo and get sent a Clip Extension), no message will be shown at all. + +### I pressed the release/collect button, but nothing happened + +It is likely that you do not have release or collect permissions, or that there is nothing to release or collect. +Another option is that your connection was interrupted. + +If you would still like to use release or collect, refer to [this section of the server commands page](https://archipelago.gg/tutorial/Archipelago/commands/en#collect/release). + +You may use the in-game console to execute the commands, if your slot has permissions to do so. + +### I am trying to configure my controller, but the menu keeps closing itself + +Steam Input will make your controller behave as a keyboard and mouse even while not playing any Steam games. + +To fix this, simply close Steam while playing Saving Princess. + +Another option is to disable Steam Input under `Steam -> Settings -> Controller -> External Gamepad Settings` 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/ItemGroups.py b/worlds/sc2/ItemGroups.py index a77fb920f64d..3a3733044579 100644 --- a/worlds/sc2/ItemGroups.py +++ b/worlds/sc2/ItemGroups.py @@ -51,7 +51,7 @@ item_name_groups.setdefault(data.type, []).append(item) # Numbered flaggroups get sorted into an unnumbered group # Currently supports numbers of one or two digits - if data.type[-2:].strip().isnumeric: + if data.type[-2:].strip().isnumeric(): type_group = data.type[:-2].strip() item_name_groups.setdefault(type_group, []).append(item) # Flaggroups with numbers are unlisted 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/Locations.py b/worlds/sc2/Locations.py index bf9c06fa3f78..b9c30bb70106 100644 --- a/worlds/sc2/Locations.py +++ b/worlds/sc2/Locations.py @@ -1387,7 +1387,7 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: lambda state: logic.templars_return_requirement(state)), LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY, lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.VICTORY, + LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.EXTRA, lambda state: logic.the_host_requirement(state)), LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA, lambda state: logic.the_host_requirement(state)), @@ -1445,11 +1445,11 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: LocationData("The Escape", "The Escape: Agent Stone", SC2NCO_LOC_ID_OFFSET + 105, LocationType.VANILLA, lambda state: logic.the_escape_requirement(state)), LocationData("Sudden Strike", "Sudden Strike: Victory", SC2NCO_LOC_ID_OFFSET + 200, LocationType.VICTORY, - lambda state: logic.sudden_strike_can_reach_objectives(state)), + lambda state: logic.sudden_strike_requirement(state)), LocationData("Sudden Strike", "Sudden Strike: Research Center", SC2NCO_LOC_ID_OFFSET + 201, LocationType.VANILLA, lambda state: logic.sudden_strike_can_reach_objectives(state)), LocationData("Sudden Strike", "Sudden Strike: Weaponry Labs", SC2NCO_LOC_ID_OFFSET + 202, LocationType.VANILLA, - lambda state: logic.sudden_strike_requirement(state)), + lambda state: logic.sudden_strike_can_reach_objectives(state)), LocationData("Sudden Strike", "Sudden Strike: Brutalisk", SC2NCO_LOC_ID_OFFSET + 203, LocationType.EXTRA, lambda state: logic.sudden_strike_requirement(state)), LocationData("Enemy Intelligence", "Enemy Intelligence: Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY, diff --git a/worlds/sc2/MissionTables.py b/worlds/sc2/MissionTables.py index 4dece46411bf..08e1f133deda 100644 --- a/worlds/sc2/MissionTables.py +++ b/worlds/sc2/MissionTables.py @@ -43,6 +43,9 @@ def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPr self.goal_priority = goal_priority self.race = race + def __lt__(self, other: "SC2Campaign"): + return self.id < other.id + GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS diff --git a/worlds/sc2/Regions.py b/worlds/sc2/Regions.py index 84830a9a32bd..273bc4a5e87c 100644 --- a/worlds/sc2/Regions.py +++ b/worlds/sc2/Regions.py @@ -50,7 +50,7 @@ def create_vanilla_regions( names: Dict[str, int] = {} # Generating all regions and locations for each enabled campaign - for campaign in enabled_campaigns: + for campaign in sorted(enabled_campaigns): for region_name in vanilla_mission_req_table[campaign].keys(): regions.append(create_region(world, locations_per_region, location_cache, region_name)) world.multiworld.regions += regions 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/sc2/requirements.txt b/worlds/sc2/requirements.txt index 9b84863c4590..5bc808b639db 100644 --- a/worlds/sc2/requirements.txt +++ b/worlds/sc2/requirements.txt @@ -1,2 +1 @@ nest-asyncio >= 1.5.5 -six >= 1.16.0 \ No newline at end of file diff --git a/worlds/shivers/Constants.py b/worlds/shivers/Constants.py index 0b00cecec3ec..95b3c2d56ad9 100644 --- a/worlds/shivers/Constants.py +++ b/worlds/shivers/Constants.py @@ -3,7 +3,7 @@ import pkgutil def load_data_file(*args) -> dict: - fname = os.path.join("data", *args) + fname = "/".join(["data", *args]) return json.loads(pkgutil.get_data(__name__, fname).decode()) location_id_offset: int = 27000 diff --git a/worlds/shivers/Items.py b/worlds/shivers/Items.py index 3b403be5cb76..10d234d450bb 100644 --- a/worlds/shivers/Items.py +++ b/worlds/shivers/Items.py @@ -33,28 +33,38 @@ class ItemData(typing.NamedTuple): "Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"), "Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"), "Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"), + "Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "pot_type2"), + "Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "pot_type2"), + "Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "pot_type2"), + "Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "pot_type2"), + "Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "pot_type2"), + "Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "pot_type2"), + "Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "pot_type2"), + "Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "pot_type2"), + "Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "pot_type2"), + "Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "pot_type2"), #Keys - "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "key"), - "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "key"), - "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "key"), - "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "key"), - "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "key"), - "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "key"), - "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "key"), - "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "key"), - "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "key"), - "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "key"), - "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), - "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), - "Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), - "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), - "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), - "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), - "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), - "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), - "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), - "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key-optional"), + "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), + "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), + "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), + "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), + "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), + "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), + "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), + "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), + "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), + "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key"), + "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, "key"), + "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, "key"), + "Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, "key"), + "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, "key"), + "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, "key"), + "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, "key"), + "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, "key"), + "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, "key"), + "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, "key"), + "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, "key-optional"), #Abilities "Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"), @@ -83,6 +93,16 @@ class ItemData(typing.NamedTuple): "Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"), "Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"), "Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"), + "Water Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 140, "potduplicate_type2"), + "Wax Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 141, "potduplicate_type2"), + "Ash Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 142, "potduplicate_type2"), + "Oil Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 143, "potduplicate_type2"), + "Cloth Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 144, "potduplicate_type2"), + "Wood Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 145, "potduplicate_type2"), + "Crystal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 146, "potduplicate_type2"), + "Lightning Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 147, "potduplicate_type2"), + "Sand Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 148, "potduplicate_type2"), + "Metal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 149, "potduplicate_type2"), #Filler "Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"), diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py index b70882f9a545..72791bef3e7b 100644 --- a/worlds/shivers/Options.py +++ b/worlds/shivers/Options.py @@ -1,21 +1,37 @@ -from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions +from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions, Range from dataclasses import dataclass +class IxupiCapturesNeeded(Range): + """ + Number of Ixupi Captures needed for goal condition. + """ + display_name = "Number of Ixupi Captures Needed" + range_start = 1 + range_end = 10 + default = 10 + class LobbyAccess(Choice): - """Chooses how keys needed to reach the lobby are placed. + """ + Chooses how keys needed to reach the lobby are placed. - Normal: Keys are placed anywhere - Early: Keys are placed early - - Local: Keys are placed locally""" + - Local: Keys are placed locally + """ display_name = "Lobby Access" option_normal = 0 option_early = 1 option_local = 2 + default = 1 class PuzzleHintsRequired(DefaultOnToggle): - """If turned on puzzle hints will be available before the corresponding puzzle is required. For example: The Shaman - Drums puzzle will be placed after access to the security cameras which give you the solution. Turning this off - allows for greater randomization.""" + """ + If turned on puzzle hints/solutions will be available before the corresponding puzzle is required. + + For example: The Red Door puzzle will be logically required only after access to the Beth's Address Book which gives you the solution. + + Turning this off allows for greater randomization. + """ display_name = "Puzzle Hints Required" class InformationPlaques(Toggle): @@ -26,7 +42,9 @@ class InformationPlaques(Toggle): display_name = "Include Information Plaques" class FrontDoorUsable(Toggle): - """Adds a key to unlock the front door of the museum.""" + """ + Adds a key to unlock the front door of the museum. + """ display_name = "Front Door Usable" class ElevatorsStaySolved(DefaultOnToggle): @@ -37,7 +55,9 @@ class ElevatorsStaySolved(DefaultOnToggle): display_name = "Elevators Stay Solved" class EarlyBeth(DefaultOnToggle): - """Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.""" + """ + Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle. + """ display_name = "Early Beth" class EarlyLightning(Toggle): @@ -47,9 +67,49 @@ class EarlyLightning(Toggle): """ display_name = "Early Lightning" +class LocationPotPieces(Choice): + """ + Chooses where pot pieces will be located within the multiworld. + - Own World: Pot pieces will be located within your own world + - Different World: Pot pieces will be located in another world + - Any World: Pot pieces will be located in any world + """ + display_name = "Location of Pot Pieces" + option_own_world = 0 + option_different_world = 1 + option_any_world = 2 + +class FullPots(Choice): + """ + Chooses if pots will be in pieces or already completed + - Pieces: Only pot pieces will be added to the item pool + - Complete: Only completed pots will be added to the item pool + - Mixed: Each pot will be randomly chosen to be pieces or already completed. + """ + display_name = "Full Pots" + option_pieces = 0 + option_complete = 1 + 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 lobby_access: LobbyAccess puzzle_hints_required: PuzzleHintsRequired include_information_plaques: InformationPlaques @@ -57,3 +117,6 @@ class ShiversOptions(PerGameCommonOptions): elevators_stay_solved: ElevatorsStaySolved early_beth: EarlyBeth early_lightning: EarlyLightning + location_pot_pieces: LocationPotPieces + full_pots: FullPots + puzzle_collect_behavior: PuzzleCollectBehavior diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 3dc4f51c47a2..5288fa2c9c3f 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -8,58 +8,58 @@ def water_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Lobby", "Region", player) or (state.can_reach("Janitor Closet", "Region", player) and cloth_capturable(state, player))) \ - and state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) + return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) or \ + state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player) def wax_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Library", "Region", player) or state.can_reach("Anansi", "Region", player)) \ - and state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) + return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) or \ + state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player) def ash_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Office", "Region", player) or state.can_reach("Burial", "Region", player)) \ - and state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) + return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) or \ + state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player) def oil_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Prehistoric", "Region", player) or state.can_reach("Tar River", "Region", player)) \ - and state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) + return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) or \ + state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player) def cloth_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Egypt", "Region", player) or state.can_reach("Burial", "Region", player) or state.can_reach("Janitor Closet", "Region", player)) \ - and state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) + return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) or \ + state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player) def wood_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Workshop", "Region", player) or state.can_reach("Blue Maze", "Region", player) or state.can_reach("Gods Room", "Region", player) or state.can_reach("Anansi", "Region", player)) \ - and state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) + return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) or \ + state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player) def crystal_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Lobby", "Region", player) or state.can_reach("Ocean", "Region", player)) \ - and state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) + return state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) or \ + state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player) def sand_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Greenhouse", "Region", player) or state.can_reach("Ocean", "Region", player)) \ - and state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) + return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) or \ + state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player) def metal_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Projector Room", "Region", player) or state.can_reach("Prehistoric", "Region", player) or state.can_reach("Bedroom", "Region", player)) \ - and state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) + return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) or \ + state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player) def lightning_capturable(state: CollectionState, player: int) -> bool: - return (first_nine_ixupi_capturable or state.multiworld.early_lightning[player].value) \ - and state.can_reach("Generator", "Region", player) \ - and state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) + return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_lightning.value) \ + and (state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) or \ + state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player)) def beths_body_available(state: CollectionState, player: int) -> bool: - return (first_nine_ixupi_capturable(state, player) or state.multiworld.early_beth[player].value) \ + return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_beth.value) \ and state.can_reach("Generator", "Region", player) @@ -123,7 +123,8 @@ def get_rules_lookup(player: int): "To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player), "To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player), "To Slide Room": lambda state: all_skull_dials_available(state, player), - "To Lobby From Slide Room": lambda state: (beths_body_available(state, player)) + "To Lobby From Slide Room": lambda state: beths_body_available(state, player), + "To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player) }, "locations_required": { "Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player), @@ -207,8 +208,10 @@ def set_rules(world: "ShiversWorld") -> None: # forbid cloth in janitor closet and oil in tar river forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Complete DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Complete DUPE", player) # Filler Item Forbids forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player) @@ -234,4 +237,8 @@ def set_rules(world: "ShiversWorld") -> None: forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player) # Set completion condition - multiworld.completion_condition[player] = lambda state: (first_nine_ixupi_capturable(state, player) and lightning_capturable(state, player)) + multiworld.completion_condition[player] = lambda state: (( + water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player) \ + + oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player) \ + + crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player) \ + + lightning_capturable(state, player)) >= world.options.ixupi_captures_needed.value) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index e43e91fb5ae3..3ca87ae164f2 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -1,3 +1,4 @@ +from typing import List from .Items import item_table, ShiversItem from .Rules import set_rules from BaseClasses import Item, Tutorial, Region, Location @@ -22,7 +23,7 @@ class ShiversWorld(World): Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual. """ - game: str = "Shivers" + game = "Shivers" topology_present = False web = ShiversWeb() options_dataclass = ShiversOptions @@ -30,7 +31,13 @@ class ShiversWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = Constants.location_name_to_id - + shivers_item_id_offset = 27000 + pot_completed_list: List[int] + + + def generate_early(self): + self.pot_completed_list = [] + def create_item(self, name: str) -> Item: data = item_table[name] return ShiversItem(name, data.classification, data.code, self.player) @@ -78,9 +85,28 @@ def create_items(self) -> None: #Add items to item pool itempool = [] for name, data in item_table.items(): - if data.type in {"pot", "key", "ability", "filler2"}: + if data.type in {"key", "ability", "filler2"}: itempool.append(self.create_item(name)) + # Pot pieces/Completed/Mixed: + for i in range(10): + if self.options.full_pots == "pieces": + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i])) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i])) + elif self.options.full_pots == "complete": + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i])) + else: + # Roll for if pieces or a complete pot will be used. + # Pot Pieces + if self.random.randint(0, 1) == 0: + self.pot_completed_list.append(0) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i])) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i])) + # Completed Pot + else: + self.pot_completed_list.append(1) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i])) + #Add Filler itempool += [self.create_item("Easier Lyre") for i in range(9)] @@ -88,7 +114,6 @@ def create_items(self) -> None: filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool) itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)] - #Place library escape items. Choose a location to place the escape item library_region = self.multiworld.get_region("Library", self.player) librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")]) @@ -123,14 +148,14 @@ def create_items(self) -> None: self.multiworld.itempool += itempool #Lobby acess: - if self.options.lobby_access == 1: + if self.options.lobby_access == "early": if lobby_access_keys == 1: self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1 self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1 self.multiworld.early_items[self.player]["Key for Office"] = 1 elif lobby_access_keys == 2: self.multiworld.early_items[self.player]["Key for Front Door"] = 1 - if self.options.lobby_access == 2: + if self.options.lobby_access == "local": if lobby_access_keys == 1: self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1 self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1 @@ -138,6 +163,12 @@ def create_items(self) -> None: elif lobby_access_keys == 2: self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1 + #Pot piece shuffle location: + if self.options.location_pot_pieces == "own_world": + self.options.local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"} + if self.options.location_pot_pieces == "different_world": + self.options.non_local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"} + def pre_fill(self) -> None: # Prefills event storage locations with duplicate pots storagelocs = [] @@ -149,7 +180,23 @@ def pre_fill(self) -> None: if loc_name.startswith("Accessible: "): storagelocs.append(self.multiworld.get_location(loc_name, self.player)) - storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + #Pot pieces/Completed/Mixed: + if self.options.full_pots == "pieces": + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + elif self.options.full_pots == "complete": + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate_type2'] + storageitems += [self.create_item("Empty") for i in range(10)] + else: + for i in range(10): + #Pieces + if self.pot_completed_list[i] == 0: + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i])] + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])] + #Complete + else: + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])] + storageitems += [self.create_item("Empty")] + storageitems += [self.create_item("Empty") for i in range(3)] state = self.multiworld.get_all_state(True) @@ -166,11 +213,14 @@ def pre_fill(self) -> None: def fill_slot_data(self) -> dict: return { - "storageplacements": self.storage_placements, - "excludedlocations": {str(excluded_location).replace('ExcludeLocations(', '').replace(')', '') for excluded_location in self.multiworld.exclude_locations.values()}, - "elevatorsstaysolved": {self.options.elevators_stay_solved.value}, - "earlybeth": {self.options.early_beth.value}, - "earlylightning": {self.options.early_lightning.value}, + "StoragePlacements": self.storage_placements, + "ExcludedLocations": list(self.options.exclude_locations.value), + "IxupiCapturesNeeded": self.options.ixupi_captures_needed.value, + "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, + "PuzzleCollectBehavior": self.options.puzzle_collect_behavior.value, } diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json index 1d62f85d2d1c..64fe3647348d 100644 --- a/worlds/shivers/data/locations.json +++ b/worlds/shivers/data/locations.json @@ -81,7 +81,7 @@ "Information Plaque: (Ocean) Poseidon", "Information Plaque: (Ocean) Colossus of Rhodes", "Information Plaque: (Ocean) Poseidon's Temple", - "Information Plaque: (Underground Maze) Subterranean World", + "Information Plaque: (Underground Maze Staircase) Subterranean World", "Information Plaque: (Underground Maze) Dero", "Information Plaque: (Egypt) Tomb of the Ixupi", "Information Plaque: (Egypt) The Sphinx", @@ -119,16 +119,6 @@ "Outside": [ "Puzzle Solved Gears", "Puzzle Solved Stone Henge", - "Ixupi Captured Water", - "Ixupi Captured Wax", - "Ixupi Captured Ash", - "Ixupi Captured Oil", - "Ixupi Captured Cloth", - "Ixupi Captured Wood", - "Ixupi Captured Crystal", - "Ixupi Captured Sand", - "Ixupi Captured Metal", - "Ixupi Captured Lightning", "Puzzle Solved Office Elevator", "Puzzle Solved Three Floor Elevator", "Puzzle Hint Found: Combo Lock in Mailbox", @@ -182,7 +172,8 @@ "Accessible: Storage: Transforming Mask" ], "Generator": [ - "Final Riddle: Beth's Body Page 17" + "Final Riddle: Beth's Body Page 17", + "Ixupi Captured Lightning" ], "Theater Back Hallways": [ "Puzzle Solved Clock Tower Door" @@ -210,6 +201,7 @@ "Information Plaque: (Ocean) Poseidon's Temple" ], "Maze Staircase": [ + "Information Plaque: (Underground Maze Staircase) Subterranean World", "Puzzle Solved Maze Door" ], "Egypt": [ @@ -305,7 +297,6 @@ ], "Tar River": [ "Accessible: Storage: Tar River", - "Information Plaque: (Underground Maze) Subterranean World", "Information Plaque: (Underground Maze) Dero" ], "Theater": [ @@ -320,6 +311,33 @@ "Skull Dial Bridge": [ "Accessible: Storage: Skull Bridge", "Puzzle Solved Skull Dial Door" + ], + "Water Capture": [ + "Ixupi Captured Water" + ], + "Wax Capture": [ + "Ixupi Captured Wax" + ], + "Ash Capture": [ + "Ixupi Captured Ash" + ], + "Oil Capture": [ + "Ixupi Captured Oil" + ], + "Cloth Capture": [ + "Ixupi Captured Cloth" + ], + "Wood Capture": [ + "Ixupi Captured Wood" + ], + "Crystal Capture": [ + "Ixupi Captured Crystal" + ], + "Sand Capture": [ + "Ixupi Captured Sand" + ], + "Metal Capture": [ + "Ixupi Captured Metal" ] } -} +} diff --git a/worlds/shivers/data/regions.json b/worlds/shivers/data/regions.json index 963d100faddb..aeb5aa737366 100644 --- a/worlds/shivers/data/regions.json +++ b/worlds/shivers/data/regions.json @@ -7,35 +7,35 @@ ["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]], ["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]], ["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]], - ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office"]], - ["Workshop", ["To Office From Workshop"]], + ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office", "To Ash Capture From Office"]], + ["Workshop", ["To Office From Workshop", "To Wood Capture From Workshop"]], ["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]], - ["Bedroom", ["To Bedroom Elevator From Bedroom"]], - ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby"]], - ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library"]], + ["Bedroom", ["To Bedroom Elevator From Bedroom", "To Metal Capture From Bedroom"]], + ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby"]], + ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library", "To Wax Capture From Library"]], ["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]], ["Generator", ["To Maintenance Tunnels From Generator"]], ["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]], ["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]], ["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]], ["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]], - ["Projector Room", ["To Theater Back Hallways From Projector Room"]], - ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric"]], - ["Greenhouse", ["To Prehistoric From Greenhouse"]], - ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean"]], + ["Projector Room", ["To Theater Back Hallways From Projector Room", "To Metal Capture From Projector Room"]], + ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric", "To Oil Capture From Prehistoric", "To Metal Capture From Prehistoric"]], + ["Greenhouse", ["To Prehistoric From Greenhouse", "To Sand Capture From Greenhouse"]], + ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean", "To Crystal Capture From Ocean", "To Sand Capture From Ocean"]], ["Maze Staircase", ["To Ocean From Maze Staircase", "To Maze From Maze Staircase"]], ["Maze", ["To Maze Staircase From Maze", "To Tar River"]], - ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River"]], - ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt"]], - ["Burial", ["To Egypt From Burial", "To Shaman From Burial"]], - ["Shaman", ["To Burial From Shaman", "To Gods Room"]], - ["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room"]], - ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi"]], + ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River", "To Oil Capture From Tar River"]], + ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt", "To Cloth Capture From Egypt"]], + ["Burial", ["To Egypt From Burial", "To Shaman From Burial", "To Ash Capture From Burial", "To Cloth Capture From Burial"]], + ["Shaman", ["To Burial From Shaman", "To Gods Room", "To Wax Capture From Shaman"]], + ["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room"]], + ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi", "To Wax Capture From Anansi", "To Wood Capture From Anansi"]], ["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]], ["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]], - ["Janitor Closet", ["To Night Staircase From Janitor Closet"]], + ["Janitor Closet", ["To Night Staircase From Janitor Closet", "To Water Capture From Janitor Closet", "To Cloth Capture From Janitor Closet"]], ["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]], - ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze"]], + ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze", "To Wood Capture From Blue Maze"]], ["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]], ["Fortune Teller", ["To Blue Maze From Fortune Teller"]], ["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]], @@ -43,7 +43,16 @@ ["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]], ["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]], ["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]], - ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]] + ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]], + ["Water Capture", []], + ["Wax Capture", []], + ["Ash Capture", []], + ["Oil Capture", []], + ["Cloth Capture", []], + ["Wood Capture", []], + ["Crystal Capture", []], + ["Sand Capture", []], + ["Metal Capture", []] ], "mandatory_connections": [ ["To Registry", "Registry"], @@ -140,6 +149,29 @@ ["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"], ["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"], ["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"], - ["To Slide Room", "Slide Room"] + ["To Slide Room", "Slide Room"], + ["To Wax Capture From Library", "Wax Capture"], + ["To Wax Capture From Shaman", "Wax Capture"], + ["To Wax Capture From Anansi", "Wax Capture"], + ["To Water Capture From Lobby", "Water Capture"], + ["To Water Capture From Janitor Closet", "Water Capture"], + ["To Ash Capture From Office", "Ash Capture"], + ["To Ash Capture From Burial", "Ash Capture"], + ["To Oil Capture From Prehistoric", "Oil Capture"], + ["To Oil Capture From Tar River", "Oil Capture"], + ["To Cloth Capture From Egypt", "Cloth Capture"], + ["To Cloth Capture From Burial", "Cloth Capture"], + ["To Cloth Capture From Janitor Closet", "Cloth Capture"], + ["To Wood Capture From Workshop", "Wood Capture"], + ["To Wood Capture From Gods Room", "Wood Capture"], + ["To Wood Capture From Anansi", "Wood Capture"], + ["To Wood Capture From Blue Maze", "Wood Capture"], + ["To Crystal Capture From Lobby", "Crystal Capture"], + ["To Crystal Capture From Ocean", "Crystal Capture"], + ["To Sand Capture From Greenhouse", "Sand Capture"], + ["To Sand Capture From Ocean", "Sand Capture"], + ["To Metal Capture From Bedroom", "Metal Capture"], + ["To Metal Capture From Projector Room", "Metal Capture"], + ["To Metal Capture From Prehistoric", "Metal Capture"] ] -} \ No newline at end of file +} diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md index a92f8a6b7911..2c56152a7a0c 100644 --- a/worlds/shivers/docs/en_Shivers.md +++ b/worlds/shivers/docs/en_Shivers.md @@ -12,8 +12,8 @@ these are randomized. Crawling has been added and is required to use any crawl s ## What is considered a location check in Shivers? -1. All puzzle solves are location checks excluding elevator puzzles. -2. All Ixupi captures are location checks excluding Lightning. +1. All puzzle solves are location checks. +2. All Ixupi captures are location checks. 3. Puzzle hints/solutions are location checks. For example, looking at the Atlantis map. 4. Optionally information plaques are location checks. @@ -23,9 +23,9 @@ If the player receives a key then the corresponding door will be unlocked. If th ## What is the victory condition? -Victory is achieved when the player captures Lightning in the generator room. +Victory is achieved when the player has captured the required number Ixupi set in their options. ## Encountered a bug? -Please contact GodlFire on Discord for bugs related to Shivers world generation.\ +Please contact GodlFire on Discord for bugs related to Shivers world generation.
Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer. diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md index 187382ef643c..c53edcdf2b57 100644 --- a/worlds/shivers/docs/setup_en.md +++ b/worlds/shivers/docs/setup_en.md @@ -5,7 +5,7 @@ - [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc - [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later -- [Shivers Randomizer](https://www.speedrun.com/shivers/resources) +- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version ## Setup ScummVM for Shivers diff --git a/worlds/shorthike/Locations.py b/worlds/shorthike/Locations.py index 319ad8f20e1b..657035a03011 100644 --- a/worlds/shorthike/Locations.py +++ b/worlds/shorthike/Locations.py @@ -328,7 +328,7 @@ class LocationInfo(TypedDict): {"name": "Boat Rental", "id": base_id + 55, "inGameId": "DadDeer[0]", - "needsShovel": False, "purchase": True, + "needsShovel": False, "purchase": 100, "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, {"name": "Boat Challenge Reward", "id": base_id + 56, 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..160b7e4ec78b 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) @@ -312,15 +313,17 @@ def remove(self, state: CollectionState, item: Item) -> bool: return super(SMWorld, self).remove(state, item) def create_item(self, name: str) -> Item: - item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], + item = next((x for x in ItemManager.Items.values() if x.Name == name), None) + if item: + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], player=self.player) + raise KeyError(f"Item {name} for {self.player_name} is invalid.") 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 +636,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 +862,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 +887,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/Options.py b/worlds/sm64ex/Options.py index 60ec4bbe13c2..9c428c99590e 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,12 +1,23 @@ import typing from dataclasses import dataclass -from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet +from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet, OptionGroup from .Items import action_item_table -class EnableCoinStars(DefaultOnToggle): - """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything. - Removes 15 locations from the pool.""" +class EnableCoinStars(Choice): + """ + Determine logic for 100 Coin Stars. + + Off - Removed from pool. You can still collect them, but they don't do anything. + Optimal for ignoring 100 Coin Stars entirely. Removes 15 locations from the pool. + + On - Kept in pool, potentially randomized. + + Vanilla - Kept in pool, but NOT randomized. + """ display_name = "Enable 100 Coin Stars" + option_off = 0 + option_on = 1 + option_vanilla = 2 class StrictCapRequirements(DefaultOnToggle): @@ -91,12 +102,11 @@ class BuddyChecks(Toggle): display_name = "Bob-omb Buddy Checks" -class ExclamationBoxes(Choice): +class ExclamationBoxes(Toggle): """Include 1Up Exclamation Boxes during randomization. Adds 29 locations to the pool.""" display_name = "Randomize 1Up !-Blocks" - option_Off = 0 - option_1Ups_Only = 1 + alias_1Ups_Only = 1 class CompletionType(Choice): @@ -128,6 +138,32 @@ class MoveRandomizerActions(OptionSet): valid_keys = [action for action in action_item_table if action != 'Double Jump'] default = valid_keys +sm64_options_groups = [ + OptionGroup("Logic Options", [ + AreaRandomizer, + BuddyChecks, + ExclamationBoxes, + ProgressiveKeys, + EnableCoinStars, + StrictCapRequirements, + StrictCannonRequirements, + ]), + OptionGroup("Ability Options", [ + EnableMoveRandomizer, + MoveRandomizerActions, + StrictMoveRequirements, + ]), + OptionGroup("Star Options", [ + AmountOfStars, + FirstBowserStarDoorCost, + BasementStarDoorCost, + SecondFloorStarDoorCost, + MIPS1Cost, + MIPS2Cost, + StarsToFinish, + ]), +] + @dataclass class SM64Options(PerGameCommonOptions): area_rando: AreaRandomizer 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/__init__.py b/worlds/sm64ex/__init__.py index 833ae56ca302..afa67f233c69 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -3,7 +3,7 @@ import json from .Items import item_table, action_item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location -from .Options import SM64Options +from .Options import sm64_options_groups, SM64Options from .Rules import set_rules from .Regions import create_regions, sm64_level_to_entrances, SM64Levels from BaseClasses import Item, Tutorial, ItemClassification, Region @@ -20,6 +20,8 @@ class SM64Web(WebWorld): ["N00byKing"] )] + option_groups = sm64_options_groups + class SM64World(World): """ @@ -55,7 +57,7 @@ def generate_early(self): for action in self.options.move_rando_actions.value: max_stars -= 1 self.move_rando_bitvec |= (1 << (action_item_table[action] - action_item_table['Double Jump'])) - if (self.options.exclamation_boxes > 0): + if self.options.exclamation_boxes: max_stars += 29 self.number_of_stars = min(self.options.amount_of_stars, max_stars) self.filler_count = max_stars - self.number_of_stars @@ -102,7 +104,11 @@ def create_items(self): # 1Up Mushrooms self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,self.filler_count)] # Power Stars - self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,self.number_of_stars)] + star_range = self.number_of_stars + # Vanilla 100 Coin stars have to removed from the pool if other max star increasing options are active. + if self.options.enable_coin_stars == "vanilla": + star_range -= 15 + self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,star_range)] # Keys if (not self.options.progressive_keys): key1 = self.create_item("Basement Key") @@ -133,7 +139,7 @@ def generate_basic(self): self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI")) self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR")) - if (self.options.exclamation_boxes == 0): + if not self.options.exclamation_boxes: self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom")) @@ -164,6 +170,23 @@ def generate_basic(self): self.multiworld.get_location("Wing Mario Over the Rainbow 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("Bowser in the Sky 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + if (self.options.enable_coin_stars == "vanilla"): + self.multiworld.get_location("BoB: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("WF: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("JRB: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("CCM: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("BBH: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("HMC: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("LLL: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("SSL: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("DDD: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("SL: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("WDW: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("TTM: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("THI: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("TTC: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + self.multiworld.get_location("RR: 100 Coins", self.player).place_locked_item(self.create_item("Power Star")) + def get_filler_item_name(self) -> str: return "1Up Mushroom" diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 5983057f7d7a..9963d3945a10 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -29,13 +29,25 @@ Then continue to `Using the Launcher` *Using the Launcher* -1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry +1. Go to the page linked for SM64AP-Launcher, and press on the topmost entry. 2. Scroll down, and download the zip file for your OS. -3. Unpack the zip file in an empty folder +3. Unpack the zip file in an empty folder. 4. Run the Launcher. On first start, press `Check Requirements`, which will guide you through the rest of the needed steps. - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually. -5. When finished, use `Compile default SM64AP build` to continue - - Advanced user can use `Show advanced options` to build with custom makeflags (`BETTERCAMERA`, `NODRAWINGDISTANCE`, ...), different repos and branches, and game patches such as 60FPS, Enhanced Moveset and others. +5. When finished, use `Compile default SM64AP build` to continue. + - **Advanced configuration:** If you want to use additional build options such as Better Camera, No Drawing Distance, etc or apply game patches such as 60FPS, Enhanced Moveset, etc, then use the `Compile custom build` option: + - Set a name for your build, e.g. "archipelago" or whatever you like. + - Press the `Download Files` button. + - Set Make Flags, e.g. `-j8 BETTERCAMERA=1 NODRAWINGDISTANCE=1` to enable Better Camera and No Drawing Distance. + - Press `Apply Patches` to select patches to apply. Example patches include: + - 60FPS: Improves frame rate. + - Enhanced Moveset: Gives Mario new abilities. [Details here](https://github.com/TheGag96/sm64-port). + - Nonstop Mode: Makes it possible to fetch multiple stars in a level without exiting the level first. + - Press `Create Build`. This will take several minutes. + - You can also use the Repository and Branch fields to build with different repos or branches if you want to build using a fork or development version of SM64AP. + - For more details, see: + - [Available Makeflags](https://github.com/sm64pc/sm64ex/wiki/Build-options) + - [Included Game Patches](https://github.com/N00byKing/sm64ex/blob/archipelago/enhancements/README.md) 6. Press `Download Files` to prepare the build, afterwards `Create Build`. 7. SM64EX will now be compiled. This can take a while. @@ -77,9 +89,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 +138,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 +163,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 ada463fa3629..02521d695a7a 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,7 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range + +from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility, StartInventoryPool +from dataclasses import dataclass class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -126,19 +128,20 @@ class EnergyBeep(DefaultOnToggle): """Toggles the low health energy beep in Super Metroid.""" display_name = "Energy Beep" - -smz3_options: typing.Dict[str, type(Option)] = { - "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): + start_inventory_from_pool: StartInventoryPool + 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/Rom.py b/worlds/smz3/Rom.py index 3fec151dc679..d66d9239792d 100644 --- a/worlds/smz3/Rom.py +++ b/worlds/smz3/Rom.py @@ -3,18 +3,38 @@ import Utils from Utils import read_snes_rom -from worlds.Files import APDeltaPatch +from worlds.Files import APProcedurePatch, APPatchExtension, APTokenMixin, APTokenTypes +from worlds.smz3.ips import IPS_Patch SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' LTTPJPN10HASH = '03a63945398191337e896e5771f77173' ROM_PLAYER_LIMIT = 256 +world_folder = os.path.dirname(__file__) -class SMZ3DeltaPatch(APDeltaPatch): +class SMZ3PatchExtensions(APPatchExtension): + game = "SMZ3" + + @staticmethod + def apply_basepatch(caller: APProcedurePatch, rom: bytes) -> bytes: + basepatch = IPS_Patch.load(world_folder + "/data/zsm.ips") + return basepatch.apply(rom) + +class SMZ3ProcedurePatch(APProcedurePatch, APTokenMixin): hash = "3a177ba9879e3dd04fb623a219d175b2" game = "SMZ3" patch_file_ending = ".apsmz3" + procedure = [ + ("apply_basepatch", []), + ("apply_tokens", ["token_data.bin"]), + ] + + def write_tokens(self, patches): + for addr, data in patches.items(): + self.write_token(APTokenTypes.WRITE, addr, bytes(data)) + self.write_file("token_data.bin", self.get_token_binary()) + @classmethod def get_source_data(cls) -> bytes: return get_base_rom_bytes() diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 6056a171d370..838db1f7e745 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -19,11 +19,10 @@ from .TotalSMZ3.Region import IReward, IMedallionAccess from .TotalSMZ3.Text.Texts import openFile from worlds.AutoWorld import World, AutoLogicRegister, WebWorld +from .Rom import SMZ3ProcedurePatch +from .Options import SMZ3Options +from Options import ItemsAccessibility 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 world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") @@ -68,7 +67,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 @@ -181,22 +182,18 @@ def isProgression(cls, itemType): } return itemType in progressionTypes - @classmethod - def stage_assert_generate(cls, multiworld: MultiWorld): - base_combined_rom = get_base_rom_bytes() - 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) @@ -215,7 +212,6 @@ def create_items(self): niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) - allJunkItems = niceItems + junkItems self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): @@ -223,12 +219,13 @@ 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)) itemPool = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in progressionItems] + \ - [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in allJunkItems] + [SMZ3Item(item.Type.name, ItemClassification.useful, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in niceItems] + \ + [SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in junkItems] self.smz3DungeonItems = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in self.dungeon] self.multiworld.itempool += itemPool @@ -244,7 +241,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] != 'locations': + 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 +402,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 +417,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], @@ -442,10 +439,6 @@ def apply_customization(self): def generate_output(self, output_directory: str): try: - base_combined_rom = get_base_rom_bytes() - basepatch = IPS_Patch.load(world_folder + "/data/zsm.ips") - base_combined_rom = basepatch.apply(base_combined_rom) - patcher = TotalSMZ3Patch(self.smz3World, [world.smz3World for key, world in self.multiworld.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")], self.multiworld.seed_name, @@ -457,21 +450,13 @@ def generate_output(self, output_directory: str): patches.update(self.apply_sm_custom_sprite()) patches.update(self.apply_item_names()) patches.update(self.apply_customization()) - for addr, bytes in patches.items(): - offset = 0 - for byte in bytes: - base_combined_rom[addr + offset] = byte - offset += 1 - - outfilebase = self.multiworld.get_out_file_name_base(self.player) - - filename = os.path.join(output_directory, f"{outfilebase}.sfc") - with open(filename, "wb") as binary_file: - binary_file.write(base_combined_rom) - patch = SMZ3DeltaPatch(os.path.splitext(filename)[0] + SMZ3DeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=filename) - patch.write() - os.remove(filename) + + patch = SMZ3ProcedurePatch(player=self.player, player_name=self.player_name) + patch.write_tokens(patches) + rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" + f"{patch.patch_file_ending}") + patch.write(rom_path) + self.rom_name = bytearray(patcher.title, 'utf8') except: raise @@ -551,7 +536,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/soe/__init__.py b/worlds/soe/__init__.py index 3baed165d821..161c749fd6bd 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -188,6 +188,7 @@ class SoEWorld(World): connect_name: str _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] + _fillers = sorted(item_name_groups["Ingredients"]) def __init__(self, multiworld: "MultiWorld", player: int): self.connect_name_available_event = threading.Event() @@ -469,7 +470,7 @@ def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None: multidata["connect_names"][self.connect_name] = payload def get_filler_item_name(self) -> str: - return self.random.choice(list(self.item_name_groups["Ingredients"])) + return self.random.choice(self._fillers) class SoEItem(Item): diff --git a/worlds/spire/Options.py b/worlds/spire/Options.py index 76cbc4cf37ae..9c94756600d6 100644 --- a/worlds/spire/Options.py +++ b/worlds/spire/Options.py @@ -1,5 +1,7 @@ import typing -from Options import TextChoice, Option, Range, Toggle +from dataclasses import dataclass + +from Options import TextChoice, Range, Toggle, PerGameCommonOptions class Character(TextChoice): @@ -55,9 +57,18 @@ class Downfall(Toggle): default = 0 -spire_options: typing.Dict[str, type(Option)] = { - "character": Character, - "ascension": Ascension, - "final_act": FinalAct, - "downfall": Downfall, -} +class DeathLink(Range): + """Percentage of health to lose when a death link is received.""" + display_name = "Death Link %" + range_start = 0 + range_end = 100 + default = 0 + + +@dataclass +class SpireOptions(PerGameCommonOptions): + character: Character + ascension: Ascension + final_act: FinalAct + downfall: Downfall + death_link: DeathLink diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 5b0e1e17f23d..a0a6a794d8a9 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -3,7 +3,7 @@ from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial from .Items import event_item_pairs, item_pool, item_table from .Locations import location_table -from .Options import spire_options +from .Options import SpireOptions from .Regions import create_regions from .Rules import set_rules from ..AutoWorld import WebWorld, World @@ -27,7 +27,8 @@ class SpireWorld(World): immense power, and Slay the Spire! """ - option_definitions = spire_options + options_dataclass = SpireOptions + options: SpireOptions game = "Slay the Spire" topology_present = False web = SpireWeb() @@ -63,15 +64,13 @@ def create_regions(self): def fill_slot_data(self) -> dict: slot_data = { - 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)) + 'seed': "".join(self.random.choice(string.ascii_letters) for i in range(16)) } - for option_name in spire_options: - option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = option.value + slot_data.update(self.options.as_dict("character", "ascension", "final_act", "downfall", "death_link")) return slot_data def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(["Card Draw", "Card Draw", "Card Draw", "Relic", "Relic"]) + return self.random.choice(["Card Draw", "Card Draw", "Card Draw", "Relic", "Relic"]) def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 61c866631690..6ba0e35e0a3a 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,39 +1,48 @@ import logging +from random import Random from typing import Dict, Any, Iterable, Optional, Union, List, TextIO -from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld from . import rules from .bundles.bundle_room import BundleRoom from .bundles.bundles import get_all_bundles +from .content import content_packs, StardewContent, unpack_content, create_content from .early_items import setup_early_items from .items import item_table, create_items, ItemData, Group, items_by_group, get_all_filler_items, remove_limited_amount_packs from .locations import location_table, create_locations, LocationData, locations_by_tag from .logic.bundle_logic import BundleLogic from .logic.logic import StardewLogic from .logic.time_logic import MAX_MONTHS -from .option_groups import sv_option_groups -from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ - BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization -from .presets import sv_options_presets +from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, NumberOfMovementBuffs, \ + BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization, FarmType, Walnutsanity +from .options.forced_options import force_change_options_if_incompatible +from .options.option_groups import sv_option_groups +from .options.presets import sv_options_presets from .regions import create_regions from .rules import set_rules -from .stardew_rule import True_, StardewRule, HasProgressionPercent +from .stardew_rule import True_, StardewRule, HasProgressionPercent, true_ from .strings.ap_names.event_names import Event from .strings.entrance_names import Entrance as EntranceName from .strings.goal_names import Goal as GoalName -from .strings.region_names import Region as RegionName +from .strings.metal_names import Ore +from .strings.region_names import Region as RegionName, LogicRegion + +logger = logging.getLogger(__name__) + +STARDEW_VALLEY = "Stardew Valley" +UNIVERSAL_TRACKER_SEED_PROPERTY = "ut_seed" client_version = 0 class StardewLocation(Location): - game: str = "Stardew Valley" + game: str = STARDEW_VALLEY class StardewItem(Item): - game: str = "Stardew Valley" + game: str = STARDEW_VALLEY class StardewWebWorld(WebWorld): @@ -58,7 +67,7 @@ class StardewValleyWorld(World): Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests, befriend villagers, and uncover dark secrets. """ - game = "Stardew Valley" + game = STARDEW_VALLEY topology_present = False item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -77,35 +86,35 @@ class StardewValleyWorld(World): options_dataclass = StardewValleyOptions options: StardewValleyOptions + content: StardewContent logic: StardewLogic web = StardewWebWorld() modified_bundles: List[BundleRoom] randomized_entrances: Dict[str, str] - total_progression_items: int - # all_progression_items: Dict[str, int] # If you need to debug total_progression_items, uncommenting this will help tremendously + total_progression_items: int + excluded_from_total_progression_items: List[str] = [Event.received_walnuts] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) self.filler_item_pool_names = [] self.total_progression_items = 0 - # self.all_progression_items = dict() + + # Taking the seed specified in slot data for UT, otherwise just generating the seed. + self.seed = getattr(multiworld, "re_gen_passthrough", {}).get(STARDEW_VALLEY, self.random.getrandbits(64)) + self.random = Random(self.seed) + + def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Optional[int]: + # If the seed is not specified in the slot data, this mean the world was generated before Universal Tracker support. + seed = slot_data.get(UNIVERSAL_TRACKER_SEED_PROPERTY) + if seed is None: + logger.warning(f"World was generated before Universal Tracker support. Tracker might not be accurate.") + return seed def generate_early(self): - self.force_change_options_if_incompatible() - - def force_change_options_if_incompatible(self): - goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter - goal_is_perfection = self.options.goal == Goal.option_perfection - goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection - exclude_ginger_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - if goal_is_island_related and exclude_ginger_island: - self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false - goal_name = self.options.goal.current_key - player_name = self.multiworld.player_name[self.player] - logging.warning( - f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") + force_change_options_if_incompatible(self.options, self.player, self.player_name) + self.content = create_content(self.options) def create_regions(self): def create_region(name: str, exits: Iterable[str]) -> Region: @@ -113,11 +122,12 @@ def create_region(name: str, exits: Iterable[str]) -> Region: region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits] return region - world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options) + world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options, self.content) - self.logic = StardewLogic(self.player, self.options, world_regions.keys()) + self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys()) self.modified_bundles = get_all_bundles(self.random, self.logic, + self.content, self.options) def add_location(name: str, code: Optional[int], region: str): @@ -125,11 +135,12 @@ def add_location(name: str, code: Optional[int], region: str): location = StardewLocation(self.player, name, code, region) region.locations.append(location) - create_locations(add_location, self.modified_bundles, self.options, self.random) + create_locations(add_location, self.modified_bundles, self.options, self.content, self.random) self.multiworld.regions.extend(world_regions.values()) def create_items(self): self.precollect_starting_season() + self.precollect_farm_type_items() items_to_exclude = [excluded_items for excluded_items in self.multiworld.precollected_items[self.player] if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, @@ -143,15 +154,26 @@ def create_items(self): for location in self.multiworld.get_locations(self.player) if location.address is not None]) - created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, - self.random) + created_items = create_items(self.create_item, locations_count, items_to_exclude, self.options, self.content, self.random) self.multiworld.itempool += created_items - setup_early_items(self.multiworld, self.options, self.player, self.random) + setup_early_items(self.multiworld, self.options, self.content, self.player, self.random) self.setup_player_events() self.setup_victory() + # This is really a best-effort to get the total progression items count. It is mostly used to spread grinds across spheres are push back locations that + # only become available after months or years in game. In most cases, not having the exact count will not impact the logic. + # + # The actual total can be impacted by the start_inventory_from_pool, when items are removed from the pool but not from the total. The is also a bug + # with plando where additional progression items can be created without being accounted for, which impact the real amount of progression items. This can + # ultimately create unwinnable seeds where some items (like Blueberry seeds) are locked in Shipsanity: Blueberry, but world is deemed winnable as the + # winning rule only check the count of collected progression items. + self.total_progression_items += sum(1 for i in self.multiworld.precollected_items[self.player] if i.advancement) + self.total_progression_items += sum(1 for i in self.multiworld.get_filled_locations(self.player) if i.advancement) + self.total_progression_items += sum(1 for i in created_items if i.advancement) + self.total_progression_items -= 1 # -1 for the victory event + def precollect_starting_season(self): if self.options.season_randomization == SeasonRandomization.option_progressive: return @@ -173,24 +195,30 @@ def precollect_starting_season(self): starting_season = self.create_starting_item(self.random.choice(season_pool)) self.multiworld.push_precollected(starting_season) + def precollect_farm_type_items(self): + if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive: + self.multiworld.push_precollected(self.create_starting_item("Progressive Coop")) + def setup_player_events(self): - self.setup_construction_events() - self.setup_quest_events() self.setup_action_events() - - def setup_construction_events(self): - can_construct_buildings = LocationData(None, RegionName.carpenter, Event.can_construct_buildings) - self.create_event_location(can_construct_buildings, True_(), Event.can_construct_buildings) - - def setup_quest_events(self): - start_dark_talisman_quest = LocationData(None, RegionName.railroad, Event.start_dark_talisman_quest) - self.create_event_location(start_dark_talisman_quest, self.logic.wallet.has_rusty_key(), Event.start_dark_talisman_quest) + self.setup_logic_events() def setup_action_events(self): - can_ship_event = LocationData(None, RegionName.shipping, Event.can_ship_items) - self.create_event_location(can_ship_event, True_(), Event.can_ship_items) - can_shop_pierre_event = LocationData(None, RegionName.pierre_store, Event.can_shop_at_pierre) - self.create_event_location(can_shop_pierre_event, True_(), Event.can_shop_at_pierre) + spring_farming = LocationData(None, LogicRegion.spring_farming, Event.spring_farming) + self.create_event_location(spring_farming, true_, Event.spring_farming) + summer_farming = LocationData(None, LogicRegion.summer_farming, Event.summer_farming) + self.create_event_location(summer_farming, true_, Event.summer_farming) + fall_farming = LocationData(None, LogicRegion.fall_farming, Event.fall_farming) + self.create_event_location(fall_farming, true_, Event.fall_farming) + winter_farming = LocationData(None, LogicRegion.winter_farming, Event.winter_farming) + self.create_event_location(winter_farming, true_, Event.winter_farming) + + def setup_logic_events(self): + def register_event(name: str, region: str, rule: StardewRule): + event_location = LocationData(None, region, name) + self.create_event_location(event_location, rule, name) + + self.logic.setup_events(register_event) def setup_victory(self): if self.options.goal == Goal.option_community_center: @@ -211,7 +239,7 @@ def setup_victory(self): Event.victory) elif self.options.goal == Goal.option_master_angler: self.create_event_location(location_table[GoalName.master_angler], - self.logic.fishing.can_catch_every_fish_in_slot(self.get_all_location_names()), + self.logic.fishing.can_catch_every_fish_for_fishsanity(), Event.victory) elif self.options.goal == Goal.option_complete_collection: self.create_event_location(location_table[GoalName.complete_museum], @@ -223,7 +251,7 @@ def setup_victory(self): Event.victory) elif self.options.goal == Goal.option_greatest_walnut_hunter: self.create_event_location(location_table[GoalName.greatest_walnut_hunter], - self.logic.has_walnut(130), + self.logic.walnut.has_walnut(130), Event.victory) elif self.options.goal == Goal.option_protector_of_the_valley: self.create_event_location(location_table[GoalName.protector_of_the_valley], @@ -270,19 +298,8 @@ def create_item(self, item: Union[str, ItemData], override_classification: ItemC if override_classification is None: override_classification = item.classification - if override_classification == ItemClassification.progression and item.name != Event.victory: - self.total_progression_items += 1 - # if item.name not in self.all_progression_items: - # self.all_progression_items[item.name] = 0 - # self.all_progression_items[item.name] += 1 return StardewItem(item.name, override_classification, item.code, self.player) - def delete_item(self, item: Item): - if item.classification & ItemClassification.progression: - self.total_progression_items -= 1 - # if item.name in self.all_progression_items: - # self.all_progression_items[item.name] -= 1 - def create_starting_item(self, item: Union[str, ItemData]) -> StardewItem: if isinstance(item, str): item = item_table[item] @@ -299,7 +316,7 @@ def create_event_location(self, location_data: LocationData, rule: StardewRule = location = StardewLocation(self.player, location_data.name, None, region) location.access_rule = rule region.locations.append(location) - location.place_locked_item(self.create_item(item)) + location.place_locked_item(StardewItem(item, ItemClassification.progression, None, self.player)) def set_rules(self): set_rules(self) @@ -358,7 +375,7 @@ def add_bundles_to_spoiler_log(self, spoiler_handle: TextIO): quality = "" else: quality = f" ({item.quality.split(' ')[0]})" - spoiler_handle.write(f"\t\t{item.amount}x {item.item_name}{quality}\n") + spoiler_handle.write(f"\t\t{item.amount}x {item.get_item()}{quality}\n") def add_entrances_to_spoiler_log(self): if self.options.entrance_randomization == EntranceRandomization.option_disabled: @@ -373,19 +390,52 @@ def fill_slot_data(self) -> Dict[str, Any]: for bundle in room.bundles: bundles[room.name][bundle.name] = {"number_required": bundle.number_required} for i, item in enumerate(bundle.items): - bundles[room.name][bundle.name][i] = f"{item.item_name}|{item.amount}|{item.quality}" + bundles[room.name][bundle.name][i] = f"{item.get_item()}|{item.amount}|{item.quality}" - excluded_options = [BundleRandomization, NumberOfMovementBuffs, NumberOfLuckBuffs] + excluded_options = [BundleRandomization, NumberOfMovementBuffs, EnabledFillerBuffs] excluded_option_names = [option.internal_name for option in excluded_options] generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints] excluded_option_names.extend(generic_option_names) included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_option_names] slot_data = self.options.as_dict(*included_option_names) slot_data.update({ + UNIVERSAL_TRACKER_SEED_PROPERTY: self.seed, "seed": self.random.randrange(1000000000), # Seed should be max 9 digits "randomized_entrances": self.randomized_entrances, "modified_bundles": bundles, - "client_version": "5.0.0", + "client_version": "6.0.0", }) return slot_data + + def collect(self, state: CollectionState, item: StardewItem) -> bool: + change = super().collect(state, item) + if not change: + return False + + walnut_amount = self.get_walnut_amount(item.name) + if walnut_amount: + state.prog_items[self.player][Event.received_walnuts] += walnut_amount + + return True + + def remove(self, state: CollectionState, item: StardewItem) -> bool: + change = super().remove(state, item) + if not change: + return False + + walnut_amount = self.get_walnut_amount(item.name) + if walnut_amount: + state.prog_items[self.player][Event.received_walnuts] -= walnut_amount + + return True + + @staticmethod + def get_walnut_amount(item_name: str) -> int: + if item_name == "Golden Walnut": + return 1 + if item_name == "3 Golden Walnuts": + return 3 + if item_name == "5 Golden Walnuts": + return 5 + return 0 diff --git a/worlds/stardew_valley/bundles/bundle.py b/worlds/stardew_valley/bundles/bundle.py index 199826b96bc8..43afc750b87a 100644 --- a/worlds/stardew_valley/bundles/bundle.py +++ b/worlds/stardew_valley/bundles/bundle.py @@ -1,8 +1,10 @@ +import math from dataclasses import dataclass from random import Random -from typing import List +from typing import List, Tuple from .bundle_item import BundleItem +from ..content import StardewContent from ..options import BundlePrice, StardewValleyOptions, ExcludeGingerIsland, FestivalLocations from ..strings.currency_names import Currency @@ -26,7 +28,8 @@ class BundleTemplate: number_possible_items: int number_required_items: int - def __init__(self, room: str, name: str, items: List[BundleItem], number_possible_items: int, number_required_items: int): + def __init__(self, room: str, name: str, items: List[BundleItem], number_possible_items: int, + number_required_items: int): self.room = room self.name = name self.items = items @@ -35,17 +38,12 @@ def __init__(self, room: str, name: str, items: List[BundleItem], number_possibl @staticmethod def extend_from(template, items: List[BundleItem]): - return BundleTemplate(template.room, template.name, items, template.number_possible_items, template.number_required_items) + return BundleTemplate(template.room, template.name, items, template.number_possible_items, + template.number_required_items) - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - if bundle_price_option == BundlePrice.option_minimum: - number_required = 1 - elif bundle_price_option == BundlePrice.option_maximum: - number_required = 8 - else: - number_required = self.number_required_items + bundle_price_option.value - number_required = max(1, number_required) - filtered_items = [item for item in self.items if item.can_appear(options)] + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False) + filtered_items = [item for item in self.items if item.can_appear(content, options)] number_items = len(filtered_items) number_chosen_items = self.number_possible_items if number_chosen_items < number_required: @@ -55,6 +53,7 @@ def create_bundle(self, bundle_price_option: BundlePrice, random: Random, option chosen_items = filtered_items + random.choices(filtered_items, k=number_chosen_items - number_items) else: chosen_items = random.sample(filtered_items, number_chosen_items) + chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items] return Bundle(self.room, self.name, chosen_items, number_required) def can_appear(self, options: StardewValleyOptions) -> bool: @@ -68,19 +67,13 @@ def __init__(self, room: str, name: str, item: BundleItem): super().__init__(room, name, [item], 1, 1) self.item = item - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - currency_amount = self.get_currency_amount(bundle_price_option) + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + currency_amount = self.get_currency_amount(options.bundle_price) return Bundle(self.room, self.name, [BundleItem(self.item.item_name, currency_amount)], 1) def get_currency_amount(self, bundle_price_option: BundlePrice): - if bundle_price_option == BundlePrice.option_minimum: - price_multiplier = 0.1 - elif bundle_price_option == BundlePrice.option_maximum: - price_multiplier = 4 - else: - price_multiplier = round(1 + (bundle_price_option.value * 0.4), 2) - - currency_amount = int(self.item.amount * price_multiplier) + _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True) + currency_amount = max(1, int(self.item.amount * price_multiplier)) return currency_amount def can_appear(self, options: StardewValleyOptions) -> bool: @@ -95,11 +88,11 @@ def can_appear(self, options: StardewValleyOptions) -> bool: class MoneyBundleTemplate(CurrencyBundleTemplate): - def __init__(self, room: str, item: BundleItem): - super().__init__(room, "", item) + def __init__(self, room: str, default_name: str, item: BundleItem): + super().__init__(room, default_name, item) - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - currency_amount = self.get_currency_amount(bundle_price_option) + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + currency_amount = self.get_currency_amount(options.bundle_price) currency_name = "g" if currency_amount >= 1000: unit_amount = currency_amount % 1000 @@ -111,13 +104,8 @@ def create_bundle(self, bundle_price_option: BundlePrice, random: Random, option return Bundle(self.room, name, [BundleItem(self.item.item_name, currency_amount)], 1) def get_currency_amount(self, bundle_price_option: BundlePrice): - if bundle_price_option == BundlePrice.option_minimum: - price_multiplier = 0.1 - elif bundle_price_option == BundlePrice.option_maximum: - price_multiplier = 4 - else: - price_multiplier = round(1 + (bundle_price_option.value * 0.4), 2) - currency_amount = int(self.item.amount * price_multiplier) + _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True) + currency_amount = max(1, int(self.item.amount * price_multiplier)) return currency_amount @@ -134,30 +122,54 @@ def can_appear(self, options: StardewValleyOptions) -> bool: class DeepBundleTemplate(BundleTemplate): categories: List[List[BundleItem]] - def __init__(self, room: str, name: str, categories: List[List[BundleItem]], number_possible_items: int, number_required_items: int): + def __init__(self, room: str, name: str, categories: List[List[BundleItem]], number_possible_items: int, + number_required_items: int): super().__init__(room, name, [], number_possible_items, number_required_items) self.categories = categories - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - if bundle_price_option == BundlePrice.option_minimum: - number_required = 1 - elif bundle_price_option == BundlePrice.option_maximum: - number_required = 8 - else: - number_required = self.number_required_items + bundle_price_option.value + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False) number_categories = len(self.categories) number_chosen_categories = self.number_possible_items if number_chosen_categories < number_required: number_chosen_categories = number_required if number_chosen_categories > number_categories: - chosen_categories = self.categories + random.choices(self.categories, k=number_chosen_categories - number_categories) + chosen_categories = self.categories + random.choices(self.categories, + k=number_chosen_categories - number_categories) else: chosen_categories = random.sample(self.categories, number_chosen_categories) chosen_items = [] for category in chosen_categories: - filtered_items = [item for item in category if item.can_appear(options)] + filtered_items = [item for item in category if item.can_appear(content, options)] chosen_items.append(random.choice(filtered_items)) + chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items] return Bundle(self.room, self.name, chosen_items, number_required) + + +def get_bundle_final_prices(bundle_price_option: BundlePrice, default_required_items: int, is_currency: bool) -> Tuple[int, float]: + number_required_items = get_number_required_items(bundle_price_option, default_required_items) + price_multiplier = get_price_multiplier(bundle_price_option, is_currency) + return number_required_items, price_multiplier + + +def get_number_required_items(bundle_price_option: BundlePrice, default_required_items: int) -> int: + if bundle_price_option == BundlePrice.option_minimum: + return 1 + if bundle_price_option == BundlePrice.option_maximum: + return 8 + number_required = default_required_items + bundle_price_option.value + return min(8, max(1, number_required)) + + +def get_price_multiplier(bundle_price_option: BundlePrice, is_currency: bool) -> float: + if bundle_price_option == BundlePrice.option_minimum: + return 0.1 if is_currency else 0.2 + if bundle_price_option == BundlePrice.option_maximum: + return 4 if is_currency else 1.4 + price_factor = 0.4 if is_currency else (0.2 if bundle_price_option.value <= 0 else 0.1) + price_multiplier_difference = bundle_price_option.value * price_factor + price_multiplier = 1 + price_multiplier_difference + return round(price_multiplier, 2) diff --git a/worlds/stardew_valley/bundles/bundle_item.py b/worlds/stardew_valley/bundles/bundle_item.py index 8aaa67c5f242..91e279d2a623 100644 --- a/worlds/stardew_valley/bundles/bundle_item.py +++ b/worlds/stardew_valley/bundles/bundle_item.py @@ -3,7 +3,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations +from ..content import StardewContent, content_packs +from ..options import StardewValleyOptions, FestivalLocations from ..strings.crop_names import Fruit from ..strings.currency_names import Currency from ..strings.quality_names import CropQuality, FishQuality, ForageQuality @@ -11,46 +12,70 @@ class BundleItemSource(ABC): @abstractmethod - def can_appear(self, options: StardewValleyOptions) -> bool: + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: ... class VanillaItemSource(BundleItemSource): - def can_appear(self, options: StardewValleyOptions) -> bool: + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: return True class IslandItemSource(BundleItemSource): - def can_appear(self, options: StardewValleyOptions) -> bool: - return options.exclude_ginger_island == ExcludeGingerIsland.option_false + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + return content_packs.ginger_island_content_pack.name in content.registered_packs class FestivalItemSource(BundleItemSource): - def can_appear(self, options: StardewValleyOptions) -> bool: + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: return options.festival_locations != FestivalLocations.option_disabled +# FIXME remove this once recipes are in content packs +class MasteryItemSource(BundleItemSource): + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + return content.features.skill_progression.are_masteries_shuffled + + +class ContentItemSource(BundleItemSource): + """This is meant to be used for items that are managed by the content packs.""" + + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + raise ValueError("This should not be called, check if the item is in the content instead.") + + @dataclass(frozen=True, order=True) class BundleItem: class Sources: vanilla = VanillaItemSource() island = IslandItemSource() festival = FestivalItemSource() + masteries = MasteryItemSource() + content = ContentItemSource() item_name: str amount: int = 1 quality: str = CropQuality.basic source: BundleItemSource = Sources.vanilla + flavor: str = None + can_have_quality: bool = True @staticmethod def money_bundle(amount: int) -> BundleItem: return BundleItem(Currency.money, amount) + def get_item(self) -> str: + if self.flavor is None: + return self.item_name + return f"{self.item_name} [{self.flavor}]" + def as_amount(self, amount: int) -> BundleItem: - return BundleItem(self.item_name, amount, self.quality, self.source) + return BundleItem(self.item_name, amount, self.quality, self.source, self.flavor) def as_quality(self, quality: str) -> BundleItem: - return BundleItem(self.item_name, self.amount, quality, self.source) + if self.can_have_quality: + return BundleItem(self.item_name, self.amount, quality, self.source, self.flavor) + return BundleItem(self.item_name, self.amount, self.quality, self.source, self.flavor) def as_quality_crop(self) -> BundleItem: amount = 5 @@ -67,7 +92,10 @@ def as_quality_forage(self) -> BundleItem: def __repr__(self): quality = "" if self.quality == CropQuality.basic else self.quality - return f"{self.amount} {quality} {self.item_name}" + return f"{self.amount} {quality} {self.get_item()}" + + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + if isinstance(self.source, ContentItemSource): + return self.get_item() in content.game_items - def can_appear(self, options: StardewValleyOptions) -> bool: - return self.source.can_appear(options) + return self.source.can_appear(content, options) diff --git a/worlds/stardew_valley/bundles/bundle_room.py b/worlds/stardew_valley/bundles/bundle_room.py index a5cdb89144f5..8068ff17ac83 100644 --- a/worlds/stardew_valley/bundles/bundle_room.py +++ b/worlds/stardew_valley/bundles/bundle_room.py @@ -3,6 +3,7 @@ from typing import List from .bundle import Bundle, BundleTemplate +from ..content import StardewContent from ..options import BundlePrice, StardewValleyOptions @@ -18,7 +19,25 @@ class BundleRoomTemplate: bundles: List[BundleTemplate] number_bundles: int - def create_bundle_room(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions): + def create_bundle_room(self, random: Random, content: StardewContent, options: StardewValleyOptions): filtered_bundles = [bundle for bundle in self.bundles if bundle.can_appear(options)] - chosen_bundles = random.sample(filtered_bundles, self.number_bundles) - return BundleRoom(self.name, [bundle.create_bundle(bundle_price_option, random, options) for bundle in chosen_bundles]) + + priority_bundles = [] + unpriority_bundles = [] + for bundle in filtered_bundles: + if bundle.name in options.bundle_plando: + priority_bundles.append(bundle) + else: + unpriority_bundles.append(bundle) + + if self.number_bundles <= len(priority_bundles): + chosen_bundles = random.sample(priority_bundles, self.number_bundles) + else: + chosen_bundles = priority_bundles + num_remaining_bundles = self.number_bundles - len(priority_bundles) + if num_remaining_bundles > len(unpriority_bundles): + chosen_bundles.extend(random.choices(unpriority_bundles, k=num_remaining_bundles)) + else: + chosen_bundles.extend(random.sample(unpriority_bundles, num_remaining_bundles)) + + return BundleRoom(self.name, [bundle.create_bundle(random, content, options) for bundle in chosen_bundles]) diff --git a/worlds/stardew_valley/bundles/bundles.py b/worlds/stardew_valley/bundles/bundles.py index 260ee17cbe82..99619e09aadf 100644 --- a/worlds/stardew_valley/bundles/bundles.py +++ b/worlds/stardew_valley/bundles/bundles.py @@ -1,65 +1,102 @@ from random import Random -from typing import List +from typing import List, Tuple -from .bundle_room import BundleRoom +from .bundle import Bundle +from .bundle_room import BundleRoom, BundleRoomTemplate +from ..content import StardewContent from ..data.bundle_data import pantry_vanilla, crafts_room_vanilla, fish_tank_vanilla, boiler_room_vanilla, bulletin_board_vanilla, vault_vanilla, \ pantry_thematic, crafts_room_thematic, fish_tank_thematic, boiler_room_thematic, bulletin_board_thematic, vault_thematic, pantry_remixed, \ crafts_room_remixed, fish_tank_remixed, boiler_room_remixed, bulletin_board_remixed, vault_remixed, all_bundle_items_except_money, \ - abandoned_joja_mart_thematic, abandoned_joja_mart_vanilla, abandoned_joja_mart_remixed + abandoned_joja_mart_thematic, abandoned_joja_mart_vanilla, abandoned_joja_mart_remixed, raccoon_vanilla, raccoon_thematic, raccoon_remixed, \ + community_center_remixed_anywhere from ..logic.logic import StardewLogic -from ..options import BundleRandomization, StardewValleyOptions, ExcludeGingerIsland +from ..options import BundleRandomization, StardewValleyOptions -def get_all_bundles(random: Random, logic: StardewLogic, options: StardewValleyOptions) -> List[BundleRoom]: +def get_all_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: if options.bundle_randomization == BundleRandomization.option_vanilla: - return get_vanilla_bundles(random, options) + return get_vanilla_bundles(random, content, options) elif options.bundle_randomization == BundleRandomization.option_thematic: - return get_thematic_bundles(random, options) + return get_thematic_bundles(random, content, options) elif options.bundle_randomization == BundleRandomization.option_remixed: - return get_remixed_bundles(random, options) + return get_remixed_bundles(random, content, options) + elif options.bundle_randomization == BundleRandomization.option_remixed_anywhere: + return get_remixed_bundles_anywhere(random, content, options) elif options.bundle_randomization == BundleRandomization.option_shuffled: - return get_shuffled_bundles(random, logic, options) + return get_shuffled_bundles(random, logic, content, options) raise NotImplementedError -def get_vanilla_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_vanilla.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_vanilla.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_vanilla.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_vanilla.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_vanilla.create_bundle_room(options.bundle_price, random, options) - vault = vault_vanilla.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_vanilla.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_thematic_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_thematic.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_thematic.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_thematic.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_thematic.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_thematic.create_bundle_room(options.bundle_price, random, options) - vault = vault_thematic.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_thematic.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_remixed_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_remixed.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_remixed.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_remixed.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_remixed.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_remixed.create_bundle_room(options.bundle_price, random, options) - vault = vault_remixed.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_shuffled_bundles(random: Random, logic: StardewLogic, options: StardewValleyOptions) -> List[BundleRoom]: - valid_bundle_items = [bundle_item for bundle_item in all_bundle_items_except_money if bundle_item.can_appear(options)] - - rooms = [room for room in get_remixed_bundles(random, options) if room.name != "Vault"] +def get_vanilla_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_vanilla.create_bundle_room(random, content, options) + crafts_room = crafts_room_vanilla.create_bundle_room(random, content, options) + fish_tank = fish_tank_vanilla.create_bundle_room(random, content, options) + boiler_room = boiler_room_vanilla.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_vanilla.create_bundle_room(random, content, options) + vault = vault_vanilla.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_vanilla.create_bundle_room(random, content, options) + raccoon = raccoon_vanilla.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_thematic_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_thematic.create_bundle_room(random, content, options) + crafts_room = crafts_room_thematic.create_bundle_room(random, content, options) + fish_tank = fish_tank_thematic.create_bundle_room(random, content, options) + boiler_room = boiler_room_thematic.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_thematic.create_bundle_room(random, content, options) + vault = vault_thematic.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_thematic.create_bundle_room(random, content, options) + raccoon = raccoon_thematic.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_remixed_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_remixed.create_bundle_room(random, content, options) + crafts_room = crafts_room_remixed.create_bundle_room(random, content, options) + fish_tank = fish_tank_remixed.create_bundle_room(random, content, options) + boiler_room = boiler_room_remixed.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_remixed.create_bundle_room(random, content, options) + vault = vault_remixed.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options) + raccoon = raccoon_remixed.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_remixed_bundles_anywhere(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + big_room = community_center_remixed_anywhere.create_bundle_room(random, content, options) + all_chosen_bundles = big_room.bundles + random.shuffle(all_chosen_bundles) + + end_index = 0 + + pantry, end_index = create_room_from_bundles(pantry_remixed, all_chosen_bundles, end_index) + crafts_room, end_index = create_room_from_bundles(crafts_room_remixed, all_chosen_bundles, end_index) + fish_tank, end_index = create_room_from_bundles(fish_tank_remixed, all_chosen_bundles, end_index) + boiler_room, end_index = create_room_from_bundles(boiler_room_remixed, all_chosen_bundles, end_index) + bulletin_board, end_index = create_room_from_bundles(bulletin_board_remixed, all_chosen_bundles, end_index) + + vault = vault_remixed.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options) + raccoon = raccoon_remixed.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def create_room_from_bundles(template: BundleRoomTemplate, all_bundles: List[Bundle], end_index: int) -> Tuple[BundleRoom, int]: + start_index = end_index + end_index += template.number_bundles + return BundleRoom(template.name, all_bundles[start_index:end_index]), end_index + + +def get_shuffled_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + valid_bundle_items = [bundle_item for bundle_item in all_bundle_items_except_money if bundle_item.can_appear(content, options)] + + rooms = [room for room in get_remixed_bundles(random, content, options) if room.name != "Vault"] required_items = 0 for room in rooms: for bundle in room.bundles: @@ -67,14 +104,21 @@ def get_shuffled_bundles(random: Random, logic: StardewLogic, options: StardewVa random.shuffle(room.bundles) random.shuffle(rooms) + # Remove duplicates of the same item + valid_bundle_items = [item1 for i, item1 in enumerate(valid_bundle_items) + if not any(item1.item_name == item2.item_name and item1.quality == item2.quality for item2 in valid_bundle_items[:i])] chosen_bundle_items = random.sample(valid_bundle_items, required_items) - sorted_bundle_items = sorted(chosen_bundle_items, key=lambda x: logic.has(x.item_name).get_difficulty()) for room in rooms: for bundle in room.bundles: num_items = len(bundle.items) - bundle.items = sorted_bundle_items[:num_items] - sorted_bundle_items = sorted_bundle_items[num_items:] + bundle.items = chosen_bundle_items[:num_items] + chosen_bundle_items = chosen_bundle_items[num_items:] - vault = vault_remixed.create_bundle_room(options.bundle_price, random, options) + vault = vault_remixed.create_bundle_room(random, content, options) return [*rooms, vault] + +def fix_raccoon_bundle_names(raccoon): + for i in range(len(raccoon.bundles)): + raccoon_bundle = raccoon.bundles[i] + raccoon_bundle.name = f"Raccoon Request {i + 1}" diff --git a/worlds/stardew_valley/content/__init__.py b/worlds/stardew_valley/content/__init__.py new file mode 100644 index 000000000000..54b4d75d5e5c --- /dev/null +++ b/worlds/stardew_valley/content/__init__.py @@ -0,0 +1,124 @@ +from . import content_packs +from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression +from .game_content import ContentPack, StardewContent, StardewFeatures +from .unpacking import unpack_content +from .. import options + + +def create_content(player_options: options.StardewValleyOptions) -> StardewContent: + active_packs = choose_content_packs(player_options) + features = choose_features(player_options) + return unpack_content(features, active_packs) + + +def choose_content_packs(player_options: options.StardewValleyOptions): + active_packs = [content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines] + + if player_options.exclude_ginger_island == options.ExcludeGingerIsland.option_false: + active_packs.append(content_packs.ginger_island_content_pack) + + if player_options.special_order_locations & options.SpecialOrderLocations.value_qi: + active_packs.append(content_packs.qi_board_content_pack) + + for mod in player_options.mods.value: + active_packs.append(content_packs.by_mod[mod]) + + return active_packs + + +def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures: + return StardewFeatures( + choose_booksanity(player_options.booksanity), + choose_cropsanity(player_options.cropsanity), + choose_fishsanity(player_options.fishsanity), + choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size), + choose_skill_progression(player_options.skill_progression), + ) + + +booksanity_by_option = { + options.Booksanity.option_none: booksanity.BooksanityDisabled(), + options.Booksanity.option_power: booksanity.BooksanityPower(), + options.Booksanity.option_power_skill: booksanity.BooksanityPowerSkill(), + options.Booksanity.option_all: booksanity.BooksanityAll(), +} + + +def choose_booksanity(booksanity_option: options.Booksanity) -> booksanity.BooksanityFeature: + booksanity_feature = booksanity_by_option.get(booksanity_option) + + if booksanity_feature is None: + raise ValueError(f"No booksanity feature mapped to {str(booksanity_option.value)}") + + return booksanity_feature + + +cropsanity_by_option = { + options.Cropsanity.option_disabled: cropsanity.CropsanityDisabled(), + options.Cropsanity.option_enabled: cropsanity.CropsanityEnabled(), +} + + +def choose_cropsanity(cropsanity_option: options.Cropsanity) -> cropsanity.CropsanityFeature: + cropsanity_feature = cropsanity_by_option.get(cropsanity_option) + + if cropsanity_feature is None: + raise ValueError(f"No cropsanity feature mapped to {str(cropsanity_option.value)}") + + return cropsanity_feature + + +fishsanity_by_option = { + options.Fishsanity.option_none: fishsanity.FishsanityNone(), + options.Fishsanity.option_legendaries: fishsanity.FishsanityLegendaries(), + options.Fishsanity.option_special: fishsanity.FishsanitySpecial(), + options.Fishsanity.option_randomized: fishsanity.FishsanityAll(randomization_ratio=0.4), + options.Fishsanity.option_all: fishsanity.FishsanityAll(), + options.Fishsanity.option_exclude_legendaries: fishsanity.FishsanityExcludeLegendaries(), + options.Fishsanity.option_exclude_hard_fish: fishsanity.FishsanityExcludeHardFish(), + options.Fishsanity.option_only_easy_fish: fishsanity.FishsanityOnlyEasyFish(), +} + + +def choose_fishsanity(fishsanity_option: options.Fishsanity) -> fishsanity.FishsanityFeature: + fishsanity_feature = fishsanity_by_option.get(fishsanity_option) + + if fishsanity_feature is None: + raise ValueError(f"No fishsanity feature mapped to {str(fishsanity_option.value)}") + + return fishsanity_feature + + +def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: options.FriendsanityHeartSize) -> friendsanity.FriendsanityFeature: + if friendsanity_option == options.Friendsanity.option_none: + return friendsanity.FriendsanityNone() + + if friendsanity_option == options.Friendsanity.option_bachelors: + return friendsanity.FriendsanityBachelors(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_starting_npcs: + return friendsanity.FriendsanityStartingNpc(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_all: + return friendsanity.FriendsanityAll(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_all_with_marriage: + return friendsanity.FriendsanityAllWithMarriage(heart_size.value) + + raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}") + + +skill_progression_by_option = { + options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(), + options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(), + options.SkillProgression.option_progressive_with_masteries: skill_progression.SkillProgressionProgressiveWithMasteries(), +} + + +def choose_skill_progression(skill_progression_option: options.SkillProgression) -> skill_progression.SkillProgressionFeature: + skill_progression_feature = skill_progression_by_option.get(skill_progression_option) + + if skill_progression_feature is None: + raise ValueError(f"No skill progression feature mapped to {str(skill_progression_option.value)}") + + return skill_progression_feature diff --git a/worlds/stardew_valley/content/content_packs.py b/worlds/stardew_valley/content/content_packs.py new file mode 100644 index 000000000000..fb8df8c70cba --- /dev/null +++ b/worlds/stardew_valley/content/content_packs.py @@ -0,0 +1,31 @@ +import importlib +import pkgutil + +from . import mods +from .mod_registry import by_mod +from .vanilla.base import base_game +from .vanilla.ginger_island import ginger_island_content_pack +from .vanilla.pelican_town import pelican_town +from .vanilla.qi_board import qi_board_content_pack +from .vanilla.the_desert import the_desert +from .vanilla.the_farm import the_farm +from .vanilla.the_mines import the_mines + +assert base_game +assert ginger_island_content_pack +assert pelican_town +assert qi_board_content_pack +assert the_desert +assert the_farm +assert the_mines + +# Dynamically register everything currently in the mods folder. This would ideally be done through a metaclass, but I have not looked into that yet. +mod_modules = pkgutil.iter_modules(mods.__path__) + +loaded_modules = {} +for mod_module in mod_modules: + module_name = mod_module.name + module = importlib.import_module("." + module_name, mods.__name__) + loaded_modules[module_name] = module + +assert by_mod diff --git a/worlds/stardew_valley/content/feature/__init__.py b/worlds/stardew_valley/content/feature/__init__.py new file mode 100644 index 000000000000..f3e5c6732e32 --- /dev/null +++ b/worlds/stardew_valley/content/feature/__init__.py @@ -0,0 +1,5 @@ +from . import booksanity +from . import cropsanity +from . import fishsanity +from . import friendsanity +from . import skill_progression diff --git a/worlds/stardew_valley/content/feature/booksanity.py b/worlds/stardew_valley/content/feature/booksanity.py new file mode 100644 index 000000000000..5eade5932535 --- /dev/null +++ b/worlds/stardew_valley/content/feature/booksanity.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Optional, Iterable + +from ...data.game_item import GameItem, ItemTag +from ...strings.book_names import ordered_lost_books + +item_prefix = "Power: " +location_prefix = "Read " + + +def to_item_name(book: str) -> str: + return item_prefix + book + + +def to_location_name(book: str) -> str: + return location_prefix + book + + +def extract_book_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +class BooksanityFeature(ABC): + is_enabled: ClassVar[bool] + + to_item_name = staticmethod(to_item_name) + progressive_lost_book = "Progressive Lost Book" + to_location_name = staticmethod(to_location_name) + extract_book_from_location_name = staticmethod(extract_book_from_location_name) + + @abstractmethod + def is_included(self, book: GameItem) -> bool: + ... + + @staticmethod + def get_randomized_lost_books() -> Iterable[str]: + return [] + + +class BooksanityDisabled(BooksanityFeature): + is_enabled = False + + def is_included(self, book: GameItem) -> bool: + return False + + +class BooksanityPower(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags + + +class BooksanityPowerSkill(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags + + +class BooksanityAll(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags + + @staticmethod + def get_randomized_lost_books() -> Iterable[str]: + return ordered_lost_books diff --git a/worlds/stardew_valley/content/feature/cropsanity.py b/worlds/stardew_valley/content/feature/cropsanity.py new file mode 100644 index 000000000000..18ef370815ee --- /dev/null +++ b/worlds/stardew_valley/content/feature/cropsanity.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Optional + +from ...data.game_item import GameItem, ItemTag + +location_prefix = "Harvest " + + +def to_location_name(crop: str) -> str: + return location_prefix + crop + + +def extract_crop_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +class CropsanityFeature(ABC): + is_enabled: ClassVar[bool] + + to_location_name = staticmethod(to_location_name) + extract_crop_from_location_name = staticmethod(extract_crop_from_location_name) + + @abstractmethod + def is_included(self, crop: GameItem) -> bool: + ... + + +class CropsanityDisabled(CropsanityFeature): + is_enabled = False + + def is_included(self, crop: GameItem) -> bool: + return False + + +class CropsanityEnabled(CropsanityFeature): + is_enabled = True + + def is_included(self, crop: GameItem) -> bool: + return ItemTag.CROPSANITY_SEED in crop.tags diff --git a/worlds/stardew_valley/content/feature/fishsanity.py b/worlds/stardew_valley/content/feature/fishsanity.py new file mode 100644 index 000000000000..02f9a632a873 --- /dev/null +++ b/worlds/stardew_valley/content/feature/fishsanity.py @@ -0,0 +1,101 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import ClassVar, Optional + +from ...data.fish_data import FishItem +from ...strings.fish_names import Fish + +location_prefix = "Fishsanity: " + + +def to_location_name(fish: str) -> str: + return location_prefix + fish + + +def extract_fish_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +@dataclass(frozen=True) +class FishsanityFeature(ABC): + is_enabled: ClassVar[bool] + + randomization_ratio: float = 1 + + to_location_name = staticmethod(to_location_name) + extract_fish_from_location_name = staticmethod(extract_fish_from_location_name) + + @property + def is_randomized(self) -> bool: + return self.randomization_ratio != 1 + + @abstractmethod + def is_included(self, fish: FishItem) -> bool: + ... + + +class FishsanityNone(FishsanityFeature): + is_enabled = False + + def is_included(self, fish: FishItem) -> bool: + return False + + +class FishsanityLegendaries(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.legendary + + +class FishsanitySpecial(FishsanityFeature): + is_enabled = True + + included_fishes = { + Fish.angler, + Fish.crimsonfish, + Fish.glacierfish, + Fish.legend, + Fish.mutant_carp, + Fish.blobfish, + Fish.lava_eel, + Fish.octopus, + Fish.scorpion_carp, + Fish.ice_pip, + Fish.super_cucumber, + Fish.dorado + } + + def is_included(self, fish: FishItem) -> bool: + return fish.name in self.included_fishes + + +class FishsanityAll(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return True + + +class FishsanityExcludeLegendaries(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return not fish.legendary + + +class FishsanityExcludeHardFish(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.difficulty < 80 + + +class FishsanityOnlyEasyFish(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.difficulty < 50 diff --git a/worlds/stardew_valley/content/feature/friendsanity.py b/worlds/stardew_valley/content/feature/friendsanity.py new file mode 100644 index 000000000000..3e1581b4e2f1 --- /dev/null +++ b/worlds/stardew_valley/content/feature/friendsanity.py @@ -0,0 +1,139 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from functools import lru_cache +from typing import Optional, Tuple, ClassVar + +from ...data.villagers_data import Villager +from ...strings.villager_names import NPC + +suffix = " <3" +location_prefix = "Friendsanity: " + + +def to_item_name(npc_name: str) -> str: + return npc_name + suffix + + +def to_location_name(npc_name: str, heart: int) -> str: + return location_prefix + npc_name + " " + str(heart) + suffix + + +pet_heart_item_name = to_item_name(NPC.pet) + + +def extract_npc_from_item_name(item_name: str) -> Optional[str]: + if not item_name.endswith(suffix): + return None + + return item_name[:-len(suffix)] + + +def extract_npc_from_location_name(location_name: str) -> Tuple[Optional[str], int]: + if not location_name.endswith(suffix): + return None, 0 + + trimmed = location_name[len(location_prefix):-len(suffix)] + last_space = trimmed.rindex(" ") + return trimmed[:last_space], int(trimmed[last_space + 1:]) + + +@lru_cache(maxsize=32) # Should not go pass 32 values if every friendsanity options are in the multi world +def get_heart_steps(max_heart: int, heart_size: int) -> Tuple[int, ...]: + return tuple(range(heart_size, max_heart + 1, heart_size)) + ((max_heart,) if max_heart % heart_size else ()) + + +@dataclass(frozen=True) +class FriendsanityFeature(ABC): + is_enabled: ClassVar[bool] + + heart_size: int + + to_item_name = staticmethod(to_item_name) + to_location_name = staticmethod(to_location_name) + pet_heart_item_name = pet_heart_item_name + extract_npc_from_item_name = staticmethod(extract_npc_from_item_name) + extract_npc_from_location_name = staticmethod(extract_npc_from_location_name) + + @abstractmethod + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + ... + + @property + def is_pet_randomized(self): + return bool(self.get_pet_randomized_hearts()) + + @abstractmethod + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + ... + + +class FriendsanityNone(FriendsanityFeature): + is_enabled = False + + def __init__(self): + super().__init__(1) + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + return () + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return () + + +@dataclass(frozen=True) +class FriendsanityBachelors(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if not villager.bachelor: + return () + + return get_heart_steps(8, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return () + + +@dataclass(frozen=True) +class FriendsanityStartingNpc(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if not villager.available: + return () + + if villager.bachelor: + return get_heart_steps(8, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) + + +@dataclass(frozen=True) +class FriendsanityAll(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if villager.bachelor: + return get_heart_steps(8, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) + + +@dataclass(frozen=True) +class FriendsanityAllWithMarriage(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if villager.bachelor: + return get_heart_steps(14, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) diff --git a/worlds/stardew_valley/content/feature/skill_progression.py b/worlds/stardew_valley/content/feature/skill_progression.py new file mode 100644 index 000000000000..1325d4b35ff2 --- /dev/null +++ b/worlds/stardew_valley/content/feature/skill_progression.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Iterable, Tuple + +from ...data.skill import Skill + + +class SkillProgressionFeature(ABC): + is_progressive: ClassVar[bool] + are_masteries_shuffled: ClassVar[bool] + + @abstractmethod + def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]: + ... + + @abstractmethod + def is_mastery_randomized(self, skill: Skill) -> bool: + ... + + +class SkillProgressionVanilla(SkillProgressionFeature): + is_progressive = False + are_masteries_shuffled = False + + def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]: + return () + + def is_mastery_randomized(self, skill: Skill) -> bool: + return False + + +class SkillProgressionProgressive(SkillProgressionFeature): + is_progressive = True + are_masteries_shuffled = False + + def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]: + return skill.level_names_by_level + + def is_mastery_randomized(self, skill: Skill) -> bool: + return False + + +class SkillProgressionProgressiveWithMasteries(SkillProgressionProgressive): + are_masteries_shuffled = True + + def is_mastery_randomized(self, skill: Skill) -> bool: + return skill.has_mastery diff --git a/worlds/stardew_valley/content/game_content.py b/worlds/stardew_valley/content/game_content.py new file mode 100644 index 000000000000..7ff3217b04ed --- /dev/null +++ b/worlds/stardew_valley/content/game_content.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union + +from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression +from ..data.fish_data import FishItem +from ..data.game_item import GameItem, ItemSource, ItemTag +from ..data.skill import Skill +from ..data.villagers_data import Villager + + +@dataclass(frozen=True) +class StardewContent: + features: StardewFeatures + registered_packs: Set[str] = field(default_factory=set) + + # regions -> To be used with can reach rule + + game_items: Dict[str, GameItem] = field(default_factory=dict) + fishes: Dict[str, FishItem] = field(default_factory=dict) + villagers: Dict[str, Villager] = field(default_factory=dict) + skills: Dict[str, Skill] = field(default_factory=dict) + quests: Dict[str, Any] = field(default_factory=dict) + + def find_sources_of_type(self, types: Union[Type[ItemSource], Tuple[Type[ItemSource]]]) -> Iterable[ItemSource]: + for item in self.game_items.values(): + for source in item.sources: + if isinstance(source, types): + yield source + + def source_item(self, item_name: str, *sources: ItemSource): + item = self.game_items.setdefault(item_name, GameItem(item_name)) + item.add_sources(sources) + + def tag_item(self, item_name: str, *tags: ItemTag): + item = self.game_items.setdefault(item_name, GameItem(item_name)) + item.add_tags(tags) + + def untag_item(self, item_name: str, tag: ItemTag): + self.game_items[item_name].tags.remove(tag) + + def find_tagged_items(self, tag: ItemTag) -> Iterable[GameItem]: + # TODO might be worth caching this, but it need to only be cached once the content is finalized... + for item in self.game_items.values(): + if tag in item.tags: + yield item + + +@dataclass(frozen=True) +class StardewFeatures: + booksanity: booksanity.BooksanityFeature + cropsanity: cropsanity.CropsanityFeature + fishsanity: fishsanity.FishsanityFeature + friendsanity: friendsanity.FriendsanityFeature + skill_progression: skill_progression.SkillProgressionFeature + + +@dataclass(frozen=True) +class ContentPack: + name: str + + dependencies: Iterable[str] = () + """ Hard requirement, generation will fail if it's missing. """ + weak_dependencies: Iterable[str] = () + """ Not a strict dependency, only used only for ordering the packs to make sure hooks are applied correctly. """ + + # items + # def item_hook + # ... + + harvest_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + """Harvest sources contains both crops and forageables, but also fruits from trees, the cave farm and stuff harvested from tapping like maple syrup.""" + + def harvest_source_hook(self, content: StardewContent): + ... + + shop_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def shop_source_hook(self, content: StardewContent): + ... + + fishes: Iterable[FishItem] = () + + def fish_hook(self, content: StardewContent): + ... + + crafting_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def crafting_hook(self, content: StardewContent): + ... + + artisan_good_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def artisan_good_hook(self, content: StardewContent): + ... + + villagers: Iterable[Villager] = () + + def villager_hook(self, content: StardewContent): + ... + + skills: Iterable[Skill] = () + + def skill_hook(self, content: StardewContent): + ... + + quests: Iterable[Any] = () + + def quest_hook(self, content: StardewContent): + ... + + def finalize_hook(self, content: StardewContent): + """Last hook called on the pack, once all other content packs have been registered. + + This is the place to do any final adjustments to the content, like adding rules based on tags applied by other packs. + """ + ... diff --git a/worlds/stardew_valley/content/mod_registry.py b/worlds/stardew_valley/content/mod_registry.py new file mode 100644 index 000000000000..c598fcbad295 --- /dev/null +++ b/worlds/stardew_valley/content/mod_registry.py @@ -0,0 +1,7 @@ +from .game_content import ContentPack + +by_mod = {} + + +def register_mod_content_pack(content_pack: ContentPack): + by_mod[content_pack.name] = content_pack diff --git a/worlds/stardew_valley/content/mods/__init__.py b/worlds/stardew_valley/content/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/content/mods/alecto.py b/worlds/stardew_valley/content/mods/alecto.py new file mode 100644 index 000000000000..c05c936de3c0 --- /dev/null +++ b/worlds/stardew_valley/content/mods/alecto.py @@ -0,0 +1,33 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import QuestRequirement +from ...mods.mod_data import ModNames +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.seed_names import DistantLandsSeed + + +class AlectoContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + if ModNames.distant_lands in content.registered_packs: + content.game_items.pop(DistantLandsSeed.void_mint) + content.game_items.pop(DistantLandsSeed.vile_ancient_fruit) + content.source_item(DistantLandsSeed.void_mint, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)),), + content.source_item(DistantLandsSeed.vile_ancient_fruit, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)), ), + + +register_mod_content_pack(ContentPack( + ModNames.alecto, + weak_dependencies=( + ModNames.distant_lands, # For Witch's order + ), + villagers=( + villagers_data.alecto, + ) + +)) diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py new file mode 100644 index 000000000000..5eb8af4cfc38 --- /dev/null +++ b/worlds/stardew_valley/content/mods/archeology.py @@ -0,0 +1,34 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ...data.artisan import MachineSource +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.craftable_names import ModMachine +from ...strings.fish_names import ModTrash +from ...strings.metal_names import all_artifacts, all_fossils +from ...strings.skill_names import ModSkill + + +class ArchaeologyContentPack(ContentPack): + def artisan_good_hook(self, content: StardewContent): + # Done as honestly there are too many display items to put into the initial registration traditionally. + display_items = all_artifacts + all_fossils + for item in display_items: + self.source_display_items(item, content) + content.source_item(ModTrash.rusty_scrap, *(MachineSource(item=artifact, machine=ModMachine.grinder) for artifact in all_artifacts)) + + def source_display_items(self, item: str, content: StardewContent): + wood_display = f"Wooden Display: {item}" + hardwood_display = f"Hardwood Display: {item}" + if item == "Trilobite": + wood_display = f"Wooden Display: Trilobite Fossil" + hardwood_display = f"Hardwood Display: Trilobite Fossil" + content.source_item(wood_display, MachineSource(item=str(item), machine=ModMachine.preservation_chamber)) + content.source_item(hardwood_display, MachineSource(item=str(item), machine=ModMachine.hardwood_preservation_chamber)) + + +register_mod_content_pack(ArchaeologyContentPack( + ModNames.archaeology, + skills=(Skill(name=ModSkill.archaeology, has_mastery=False),), + +)) diff --git a/worlds/stardew_valley/content/mods/big_backpack.py b/worlds/stardew_valley/content/mods/big_backpack.py new file mode 100644 index 000000000000..27b4ea1f816c --- /dev/null +++ b/worlds/stardew_valley/content/mods/big_backpack.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.big_backpack, +)) diff --git a/worlds/stardew_valley/content/mods/boarding_house.py b/worlds/stardew_valley/content/mods/boarding_house.py new file mode 100644 index 000000000000..f3ad138fa7c2 --- /dev/null +++ b/worlds/stardew_valley/content/mods/boarding_house.py @@ -0,0 +1,13 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.boarding_house, + villagers=( + villagers_data.gregory, + villagers_data.sheila, + villagers_data.joel, + ) +)) diff --git a/worlds/stardew_valley/content/mods/deepwoods.py b/worlds/stardew_valley/content/mods/deepwoods.py new file mode 100644 index 000000000000..a78629da57c0 --- /dev/null +++ b/worlds/stardew_valley/content/mods/deepwoods.py @@ -0,0 +1,28 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.harvest import ForagingSource +from ...mods.mod_data import ModNames +from ...strings.crop_names import Fruit +from ...strings.flower_names import Flower +from ...strings.region_names import DeepWoodsRegion +from ...strings.season_names import Season + +register_mod_content_pack(ContentPack( + ModNames.deepwoods, + harvest_sources={ + # Deep enough to have seen such a tree at least once + Fruit.apple: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.apricot: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.cherry: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.orange: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.peach: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.pomegranate: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.mango: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + + Flower.tulip: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.blue_jazz: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Flower.summer_spangle: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.poppy: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.fairy_rose: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + } +)) diff --git a/worlds/stardew_valley/content/mods/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py new file mode 100644 index 000000000000..c5614d130250 --- /dev/null +++ b/worlds/stardew_valley/content/mods/distant_lands.py @@ -0,0 +1,42 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import QuestRequirement +from ...mods.mod_data import ModNames +from ...strings.crop_names import DistantLandsCrop +from ...strings.forageable_names import DistantLandsForageable +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import DistantLandsSeed + + +class DistantLandsContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.untag_item(DistantLandsSeed.void_mint, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(DistantLandsSeed.vile_ancient_fruit, tag=ItemTag.CROPSANITY_SEED) + + +register_mod_content_pack(DistantLandsContentPack( + ModNames.distant_lands, + fishes=( + fish_data.void_minnow, + fish_data.purple_algae, + fish_data.swamp_leech, + fish_data.giant_horsehoe_crab, + ), + villagers=( + villagers_data.zic, + ), + harvest_sources={ + DistantLandsForageable.swamp_herb: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsForageable.brown_amanita: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsSeed.void_mint: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.void_mint: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=DistantLandsSeed.void_mint, seasons=(Season.spring, Season.summer, Season.fall)),), + DistantLandsSeed.vile_ancient_fruit: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.vile_ancient_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=DistantLandsSeed.vile_ancient_fruit, seasons=(Season.spring, Season.summer, Season.fall)),) + } +)) diff --git a/worlds/stardew_valley/content/mods/jasper.py b/worlds/stardew_valley/content/mods/jasper.py new file mode 100644 index 000000000000..146b291d800a --- /dev/null +++ b/worlds/stardew_valley/content/mods/jasper.py @@ -0,0 +1,14 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ..override import override +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.jasper, + villagers=( + villagers_data.jasper, + override(villagers_data.gunther, mod_name=ModNames.jasper), + override(villagers_data.marlon, mod_name=ModNames.jasper), + ) +)) diff --git a/worlds/stardew_valley/content/mods/magic.py b/worlds/stardew_valley/content/mods/magic.py new file mode 100644 index 000000000000..aae3617cb00c --- /dev/null +++ b/worlds/stardew_valley/content/mods/magic.py @@ -0,0 +1,10 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.magic, + skills=(Skill(name=ModSkill.magic, has_mastery=False),) +)) diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py new file mode 100644 index 000000000000..52d97d5c52b7 --- /dev/null +++ b/worlds/stardew_valley/content/mods/npc_mods.py @@ -0,0 +1,81 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.alec, + villagers=( + villagers_data.alec, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.ayeisha, + villagers=( + villagers_data.ayeisha, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.delores, + villagers=( + villagers_data.delores, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.eugene, + villagers=( + villagers_data.eugene, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.juna, + villagers=( + villagers_data.juna, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.ginger, + villagers=( + villagers_data.kitty, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.shiko, + villagers=( + villagers_data.shiko, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.wellwick, + villagers=( + villagers_data.wellwick, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.yoba, + villagers=( + villagers_data.yoba, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.riley, + villagers=( + villagers_data.riley, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.lacey, + villagers=( + villagers_data.lacey, + ) +)) diff --git a/worlds/stardew_valley/content/mods/skill_mods.py b/worlds/stardew_valley/content/mods/skill_mods.py new file mode 100644 index 000000000000..7f88b2ebf2dc --- /dev/null +++ b/worlds/stardew_valley/content/mods/skill_mods.py @@ -0,0 +1,25 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.luck_skill, + skills=(Skill(name=ModSkill.luck, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.socializing_skill, + skills=(Skill(name=ModSkill.socializing, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.cooking_skill, + skills=(Skill(name=ModSkill.cooking, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.binning_skill, + skills=(Skill(name=ModSkill.binning, has_mastery=False),) +)) diff --git a/worlds/stardew_valley/content/mods/skull_cavern_elevator.py b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py new file mode 100644 index 000000000000..ff8c089608e5 --- /dev/null +++ b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.skull_cavern_elevator, +)) diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py new file mode 100644 index 000000000000..a68d4ae9c097 --- /dev/null +++ b/worlds/stardew_valley/content/mods/sve.py @@ -0,0 +1,210 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ..override import override +from ..vanilla.ginger_island import ginger_island_content_pack as ginger_island_content_pack +from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import YearRequirement, CombatRequirement, RelationshipRequirement, ToolRequirement, SkillRequirement, FishingRequirement +from ...data.shop import ShopSource +from ...mods.mod_data import ModNames +from ...strings.craftable_names import ModEdible +from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit +from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem +from ...strings.flower_names import Flower +from ...strings.food_names import SVEMeal, SVEBeverage +from ...strings.forageable_names import Mushroom, Forageable, SVEForage +from ...strings.gift_names import SVEGift +from ...strings.metal_names import Ore +from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.performance_names import Performance +from ...strings.region_names import Region, SVERegion, LogicRegion +from ...strings.season_names import Season +from ...strings.seed_names import SVESeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial +from ...strings.villager_names import ModNPC + + +class SVEContentPack(ContentPack): + + def fish_hook(self, content: StardewContent): + if ginger_island_content_pack.name not in content.registered_packs: + content.fishes.pop(fish_data.baby_lunaloo.name) + content.fishes.pop(fish_data.clownfish.name) + content.fishes.pop(fish_data.lunaloo.name) + content.fishes.pop(fish_data.seahorse.name) + content.fishes.pop(fish_data.shiny_lunaloo.name) + content.fishes.pop(fish_data.starfish.name) + content.fishes.pop(fish_data.sea_sponge.name) + + # Remove Highlands fishes at it requires 2 Lance hearts for the quest to access it + content.fishes.pop(fish_data.daggerfish.name) + content.fishes.pop(fish_data.gemfish.name) + + # Remove Fable Reef fishes at it requires 8 Lance hearts for the event to access it + content.fishes.pop(fish_data.torpedo_trout.name) + + def villager_hook(self, content: StardewContent): + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge + content.villagers.pop(villagers_data.lance.name) + + def harvest_source_hook(self, content: StardewContent): + content.untag_item(SVESeed.shrub, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.fungus, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.slime, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.stalk, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.void, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.ancient_fern, tag=ItemTag.CROPSANITY_SEED) + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Highlands seeds as these are behind Lance existing. + content.game_items.pop(SVESeed.void) + content.game_items.pop(SVEVegetable.void_root) + content.game_items.pop(SVESeed.stalk) + content.game_items.pop(SVEFruit.monster_fruit) + content.game_items.pop(SVESeed.fungus) + content.game_items.pop(SVEVegetable.monster_mushroom) + content.game_items.pop(SVESeed.slime) + content.game_items.pop(SVEFruit.slime_berry) + + +register_mod_content_pack(SVEContentPack( + ModNames.sve, + weak_dependencies=( + ginger_island_content_pack.name, + ModNames.jasper, # To override Marlon and Gunther + ), + shop_sources={ + SVEGift.aged_blue_moon_wine: (ShopSource(money_price=28000, shop_region=SVERegion.blue_moon_vineyard),), + SVEGift.blue_moon_wine: (ShopSource(money_price=3000, shop_region=SVERegion.blue_moon_vineyard),), + ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),), + SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), + ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),), + ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),), + SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),), + SVEMeal.stamina_capsule: (ShopSource(money_price=4000, shop_region=Region.hospital),), + }, + harvest_sources={ + Mushroom.red: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.purple: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), ) + ), + Mushroom.morel: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.chanterelle: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Flower.tulip: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),), + Flower.blue_jazz: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),), + Flower.summer_spangle: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), + Flower.sunflower: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), + Flower.fairy_rose: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.fall,)),), + Fruit.ancient_fruit: ( + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring_cave,)), + ), + Fruit.sweet_gem_berry: ( + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), + ), + + # New items + + ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),), + ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,), + other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), + ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),), + ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),), + SVEForage.bearberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.winter,)),), + SVEForage.poison_mushroom: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.fall)),), + SVEForage.red_baneberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.summer)),), + SVEForage.ferngill_primrose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.spring,)),), + SVEForage.goldenrod: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.summer, Season.fall)),), + SVEForage.conch: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,)),), + SVEForage.dewdrop_berry: (ForagingSource(regions=(SVERegion.enchanted_grove,)),), + SVEForage.sand_dollar: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,), seasons=(Season.spring, Season.summer)),), + SVEForage.golden_ocean_flower: (ForagingSource(regions=(SVERegion.fable_reef,)),), + SVEForage.four_leaf_clover: (ForagingSource(regions=(Region.secret_woods, SVERegion.forest_west,), seasons=(Season.summer, Season.fall)),), + SVEForage.mushroom_colony: (ForagingSource(regions=(Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west,), seasons=(Season.fall,)),), + SVEForage.rusty_blade: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + SVEForage.rafflesia: (ForagingSource(regions=(Region.secret_woods,), seasons=Season.not_winter),), + SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),), + ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,), + other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), + SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),), + + # Fable Reef + WaterItem.coral: (ForagingSource(regions=(SVERegion.fable_reef,)),), + Forageable.rainbow_shell: (ForagingSource(regions=(SVERegion.fable_reef,)),), + WaterItem.sea_urchin: (ForagingSource(regions=(SVERegion.fable_reef,)),), + + # Crops + SVESeed.shrub: (ForagingSource(regions=(Region.secret_woods,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.salal_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.shrub, seasons=(Season.spring,)),), + SVESeed.slime: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.slime_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.slime, seasons=(Season.spring,)),), + SVESeed.ancient_fern: (ForagingSource(regions=(Region.secret_woods,)),), + SVEVegetable.ancient_fiber: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.ancient_fern, seasons=(Season.summer,)),), + SVESeed.stalk: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.monster_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.stalk, seasons=(Season.summer,)),), + SVESeed.fungus: (ForagingSource(regions=(SVERegion.highlands_pond,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.monster_mushroom: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.fungus, seasons=(Season.fall,)),), + SVESeed.void: (ForagingSource(regions=(SVERegion.highlands_cavern,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.void_root: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.void, seasons=(Season.winter,)),), + + }, + fishes=( + fish_data.baby_lunaloo, # Removed when no ginger island + fish_data.bonefish, + fish_data.bull_trout, + fish_data.butterfish, + fish_data.clownfish, # Removed when no ginger island + fish_data.daggerfish, + fish_data.frog, + fish_data.gemfish, + fish_data.goldenfish, + fish_data.grass_carp, + fish_data.king_salmon, + fish_data.kittyfish, + fish_data.lunaloo, # Removed when no ginger island + fish_data.meteor_carp, + fish_data.minnow, + fish_data.puppyfish, + fish_data.radioactive_bass, + fish_data.seahorse, # Removed when no ginger island + fish_data.shiny_lunaloo, # Removed when no ginger island + fish_data.snatcher_worm, + fish_data.starfish, # Removed when no ginger island + fish_data.torpedo_trout, + fish_data.undeadfish, + fish_data.void_eel, + fish_data.water_grub, + fish_data.sea_sponge, # Removed when no ginger island + + ), + villagers=( + villagers_data.claire, + villagers_data.lance, # Removed when no ginger island + villagers_data.mommy, + villagers_data.sophia, + villagers_data.victor, + villagers_data.andy, + villagers_data.apples, + villagers_data.gunther, + villagers_data.martin, + villagers_data.marlon, + villagers_data.morgan, + villagers_data.scarlett, + villagers_data.susan, + villagers_data.morris, + # The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando! + override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve), + ) +)) diff --git a/worlds/stardew_valley/content/mods/tractor.py b/worlds/stardew_valley/content/mods/tractor.py new file mode 100644 index 000000000000..8f143001791c --- /dev/null +++ b/worlds/stardew_valley/content/mods/tractor.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.tractor, +)) diff --git a/worlds/stardew_valley/content/override.py b/worlds/stardew_valley/content/override.py new file mode 100644 index 000000000000..adfc64c95b49 --- /dev/null +++ b/worlds/stardew_valley/content/override.py @@ -0,0 +1,7 @@ +from typing import Any + + +def override(content: Any, **kwargs) -> Any: + attributes = dict(content.__dict__) + attributes.update(kwargs) + return type(content)(**attributes) diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py new file mode 100644 index 000000000000..3c57f91afe3a --- /dev/null +++ b/worlds/stardew_valley/content/unpacking.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from graphlib import TopologicalSorter +from typing import Iterable, Mapping, Callable + +from .game_content import StardewContent, ContentPack, StardewFeatures +from .vanilla.base import base_game as base_game_content_pack +from ..data.game_item import GameItem, ItemSource + + +def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent: + # Base game is always registered first. + content = StardewContent(features) + packs_to_finalize = [base_game_content_pack] + register_pack(content, base_game_content_pack) + + # Content packs are added in order based on their dependencies + sorter = TopologicalSorter() + packs_by_name = {p.name: p for p in packs} + + # Build the dependency graph + for name, pack in packs_by_name.items(): + sorter.add(name, + *pack.dependencies, + *(wd for wd in pack.weak_dependencies if wd in packs_by_name)) + + # Graph is traversed in BFS + sorter.prepare() + while sorter.is_active(): + # Packs get shuffled in TopologicalSorter, most likely due to hash seeding. + for pack_name in sorted(sorter.get_ready()): + pack = packs_by_name[pack_name] + register_pack(content, pack) + sorter.done(pack_name) + packs_to_finalize.append(pack) + + prune_inaccessible_items(content) + + for pack in packs_to_finalize: + pack.finalize_hook(content) + + # Maybe items without source should be removed at some point + return content + + +def register_pack(content: StardewContent, pack: ContentPack): + # register regions + + # register entrances + + register_sources_and_call_hook(content, pack.harvest_sources, pack.harvest_source_hook) + register_sources_and_call_hook(content, pack.shop_sources, pack.shop_source_hook) + register_sources_and_call_hook(content, pack.crafting_sources, pack.crafting_hook) + register_sources_and_call_hook(content, pack.artisan_good_sources, pack.artisan_good_hook) + + for fish in pack.fishes: + content.fishes[fish.name] = fish + pack.fish_hook(content) + + for villager in pack.villagers: + content.villagers[villager.name] = villager + pack.villager_hook(content) + + for skill in pack.skills: + content.skills[skill.name] = skill + pack.skill_hook(content) + + # register_quests + + # ... + + content.registered_packs.add(pack.name) + + +def register_sources_and_call_hook(content: StardewContent, + sources_by_item_name: Mapping[str, Iterable[ItemSource]], + hook: Callable[[StardewContent], None]): + for item_name, sources in sources_by_item_name.items(): + item = content.game_items.setdefault(item_name, GameItem(item_name)) + item.add_sources(sources) + + for source in sources: + for requirement_name, tags in source.requirement_tags.items(): + requirement_item = content.game_items.setdefault(requirement_name, GameItem(requirement_name)) + requirement_item.add_tags(tags) + + hook(content) + + +def prune_inaccessible_items(content: StardewContent): + for item in list(content.game_items.values()): + if not item.sources: + content.game_items.pop(item.name) diff --git a/worlds/stardew_valley/content/vanilla/__init__.py b/worlds/stardew_valley/content/vanilla/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/content/vanilla/base.py b/worlds/stardew_valley/content/vanilla/base.py new file mode 100644 index 000000000000..2c910df5d00f --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/base.py @@ -0,0 +1,172 @@ +from ..game_content import ContentPack, StardewContent +from ...data.artisan import MachineSource +from ...data.game_item import ItemTag, CustomRuleSource, GameItem +from ...data.harvest import HarvestFruitTreeSource, HarvestCropSource +from ...data.skill import Skill +from ...strings.artisan_good_names import ArtisanGood +from ...strings.craftable_names import WildSeeds +from ...strings.crop_names import Fruit, Vegetable +from ...strings.flower_names import Flower +from ...strings.food_names import Beverage +from ...strings.forageable_names import all_edible_mushrooms, Mushroom, Forageable +from ...strings.fruit_tree_names import Sapling +from ...strings.machine_names import Machine +from ...strings.monster_names import Monster +from ...strings.season_names import Season +from ...strings.seed_names import Seed +from ...strings.skill_names import Skill as SkillName + +all_fruits = ( + Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Fruit.banana, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry, + Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.mango, Fruit.melon, Fruit.orange, Fruit.peach, + Fruit.pineapple, Fruit.pomegranate, Fruit.powdermelon, Fruit.qi_fruit, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit, + Fruit.strawberry +) + +all_vegetables = ( + Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower, + Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.hops, Vegetable.kale, + Vegetable.parsnip, Vegetable.potato, Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.taro_root, + Vegetable.tea_leaves, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.wheat, Vegetable.yam +) + +non_juiceable_vegetables = (Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat, Vegetable.tea_leaves) + + +# This will hold items, skills and stuff that is available everywhere across the game, but not directly needing pelican town (crops, ore, foraging, etc.) +class BaseGameContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + coffee_starter = content.game_items[Seed.coffee_starter] + content.game_items[Seed.coffee_starter] = GameItem(Seed.coffee, sources=coffee_starter.sources, tags=coffee_starter.tags) + + content.untag_item(WildSeeds.ancient, ItemTag.CROPSANITY_SEED) + + for fruit in all_fruits: + content.tag_item(fruit, ItemTag.FRUIT) + + for vegetable in all_vegetables: + content.tag_item(vegetable, ItemTag.VEGETABLE) + + for edible_mushroom in all_edible_mushrooms: + if edible_mushroom == Mushroom.magma_cap: + continue + + content.tag_item(edible_mushroom, ItemTag.EDIBLE_MUSHROOM) + + def finalize_hook(self, content: StardewContent): + # FIXME I hate this design. A listener design pattern would be more appropriate so artisan good are register at the exact moment a FRUIT tag is added. + for fruit in tuple(content.find_tagged_items(ItemTag.FRUIT)): + wine = ArtisanGood.specific_wine(fruit.name) + content.source_item(wine, MachineSource(item=fruit.name, machine=Machine.keg)) + content.source_item(ArtisanGood.wine, MachineSource(item=fruit.name, machine=Machine.keg)) + + if fruit.name == Fruit.grape: + content.source_item(ArtisanGood.raisins, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + else: + dried_fruit = ArtisanGood.specific_dried_fruit(fruit.name) + content.source_item(dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + content.source_item(ArtisanGood.dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + + jelly = ArtisanGood.specific_jelly(fruit.name) + content.source_item(jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar)) + content.source_item(ArtisanGood.jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar)) + + for vegetable in tuple(content.find_tagged_items(ItemTag.VEGETABLE)): + if vegetable.name not in non_juiceable_vegetables: + juice = ArtisanGood.specific_juice(vegetable.name) + content.source_item(juice, MachineSource(item=vegetable.name, machine=Machine.keg)) + content.source_item(ArtisanGood.juice, MachineSource(item=vegetable.name, machine=Machine.keg)) + + pickles = ArtisanGood.specific_pickles(vegetable.name) + content.source_item(pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar)) + content.source_item(ArtisanGood.pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar)) + + for mushroom in tuple(content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM)): + dried_mushroom = ArtisanGood.specific_dried_mushroom(mushroom.name) + content.source_item(dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator)) + content.source_item(ArtisanGood.dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator)) + + # for fish in tuple(content.find_tagged_items(ItemTag.FISH)): + # smoked_fish = ArtisanGood.specific_smoked_fish(fish.name) + # content.source_item(smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker)) + # content.source_item(ArtisanGood.smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker)) + + +base_game = BaseGameContentPack( + "Base game (Vanilla)", + harvest_sources={ + # Fruit tree + Fruit.apple: (HarvestFruitTreeSource(sapling=Sapling.apple, seasons=(Season.fall,)),), + Fruit.apricot: (HarvestFruitTreeSource(sapling=Sapling.apricot, seasons=(Season.spring,)),), + Fruit.cherry: (HarvestFruitTreeSource(sapling=Sapling.cherry, seasons=(Season.spring,)),), + Fruit.orange: (HarvestFruitTreeSource(sapling=Sapling.orange, seasons=(Season.summer,)),), + Fruit.peach: (HarvestFruitTreeSource(sapling=Sapling.peach, seasons=(Season.summer,)),), + Fruit.pomegranate: (HarvestFruitTreeSource(sapling=Sapling.pomegranate, seasons=(Season.fall,)),), + + # Crops + Vegetable.parsnip: (HarvestCropSource(seed=Seed.parsnip, seasons=(Season.spring,)),), + Vegetable.green_bean: (HarvestCropSource(seed=Seed.bean, seasons=(Season.spring,)),), + Vegetable.cauliflower: (HarvestCropSource(seed=Seed.cauliflower, seasons=(Season.spring,)),), + Vegetable.potato: (HarvestCropSource(seed=Seed.potato, seasons=(Season.spring,)),), + Flower.tulip: (HarvestCropSource(seed=Seed.tulip, seasons=(Season.spring,)),), + Vegetable.kale: (HarvestCropSource(seed=Seed.kale, seasons=(Season.spring,)),), + Flower.blue_jazz: (HarvestCropSource(seed=Seed.jazz, seasons=(Season.spring,)),), + Vegetable.garlic: (HarvestCropSource(seed=Seed.garlic, seasons=(Season.spring,)),), + Vegetable.unmilled_rice: (HarvestCropSource(seed=Seed.rice, seasons=(Season.spring,)),), + + Fruit.melon: (HarvestCropSource(seed=Seed.melon, seasons=(Season.summer,)),), + Vegetable.tomato: (HarvestCropSource(seed=Seed.tomato, seasons=(Season.summer,)),), + Fruit.blueberry: (HarvestCropSource(seed=Seed.blueberry, seasons=(Season.summer,)),), + Fruit.hot_pepper: (HarvestCropSource(seed=Seed.pepper, seasons=(Season.summer,)),), + Vegetable.wheat: (HarvestCropSource(seed=Seed.wheat, seasons=(Season.summer, Season.fall)),), + Vegetable.radish: (HarvestCropSource(seed=Seed.radish, seasons=(Season.summer,)),), + Flower.poppy: (HarvestCropSource(seed=Seed.poppy, seasons=(Season.summer,)),), + Flower.summer_spangle: (HarvestCropSource(seed=Seed.spangle, seasons=(Season.summer,)),), + Vegetable.hops: (HarvestCropSource(seed=Seed.hops, seasons=(Season.summer,)),), + Vegetable.corn: (HarvestCropSource(seed=Seed.corn, seasons=(Season.summer, Season.fall)),), + Flower.sunflower: (HarvestCropSource(seed=Seed.sunflower, seasons=(Season.summer, Season.fall)),), + Vegetable.red_cabbage: (HarvestCropSource(seed=Seed.red_cabbage, seasons=(Season.summer,)),), + + Vegetable.eggplant: (HarvestCropSource(seed=Seed.eggplant, seasons=(Season.fall,)),), + Vegetable.pumpkin: (HarvestCropSource(seed=Seed.pumpkin, seasons=(Season.fall,)),), + Vegetable.bok_choy: (HarvestCropSource(seed=Seed.bok_choy, seasons=(Season.fall,)),), + Vegetable.yam: (HarvestCropSource(seed=Seed.yam, seasons=(Season.fall,)),), + Fruit.cranberries: (HarvestCropSource(seed=Seed.cranberry, seasons=(Season.fall,)),), + Flower.fairy_rose: (HarvestCropSource(seed=Seed.fairy, seasons=(Season.fall,)),), + Vegetable.amaranth: (HarvestCropSource(seed=Seed.amaranth, seasons=(Season.fall,)),), + Fruit.grape: (HarvestCropSource(seed=Seed.grape, seasons=(Season.fall,)),), + Vegetable.artichoke: (HarvestCropSource(seed=Seed.artichoke, seasons=(Season.fall,)),), + + Vegetable.broccoli: (HarvestCropSource(seed=Seed.broccoli, seasons=(Season.fall,)),), + Vegetable.carrot: (HarvestCropSource(seed=Seed.carrot, seasons=(Season.spring,)),), + Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.summer,)),), + Vegetable.summer_squash: (HarvestCropSource(seed=Seed.summer_squash, seasons=(Season.summer,)),), + + Fruit.strawberry: (HarvestCropSource(seed=Seed.strawberry, seasons=(Season.spring,)),), + Fruit.sweet_gem_berry: (HarvestCropSource(seed=Seed.rare_seed, seasons=(Season.fall,)),), + Fruit.ancient_fruit: (HarvestCropSource(seed=WildSeeds.ancient, seasons=(Season.spring, Season.summer, Season.fall,)),), + + Seed.coffee_starter: (CustomRuleSource(lambda logic: logic.traveling_merchant.has_days(3) & logic.monster.can_kill_many(Monster.dust_sprite)),), + Seed.coffee: (HarvestCropSource(seed=Seed.coffee_starter, seasons=(Season.spring, Season.summer,)),), + + Vegetable.tea_leaves: (CustomRuleSource(lambda logic: logic.has(Sapling.tea) & logic.time.has_lived_months(2) & logic.season.has_any_not_winter()),), + }, + artisan_good_sources={ + Beverage.beer: (MachineSource(item=Vegetable.wheat, machine=Machine.keg),), + # Ingredient.vinegar: (MachineSource(item=Ingredient.rice, machine=Machine.keg),), + Beverage.coffee: (MachineSource(item=Seed.coffee, machine=Machine.keg), + CustomRuleSource(lambda logic: logic.has(Machine.coffee_maker)), + CustomRuleSource(lambda logic: logic.has("Hot Java Ring"))), + ArtisanGood.green_tea: (MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg),), + ArtisanGood.mead: (MachineSource(item=ArtisanGood.honey, machine=Machine.keg),), + ArtisanGood.pale_ale: (MachineSource(item=Vegetable.hops, machine=Machine.keg),), + }, + skills=( + Skill(SkillName.farming, has_mastery=True), + Skill(SkillName.foraging, has_mastery=True), + Skill(SkillName.fishing, has_mastery=True), + Skill(SkillName.mining, has_mastery=True), + Skill(SkillName.combat, has_mastery=True), + ) +) diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py new file mode 100644 index 000000000000..2fbcb032799e --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -0,0 +1,85 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack, StardewContent +from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ...data.requirement import WalnutRequirement +from ...data.shop import ShopSource +from ...strings.book_names import Book +from ...strings.crop_names import Fruit, Vegetable +from ...strings.fish_names import Fish +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.fruit_tree_names import Sapling +from ...strings.metal_names import Fossil, Mineral +from ...strings.region_names import Region, LogicRegion +from ...strings.season_names import Season +from ...strings.seed_names import Seed + + +class GingerIslandContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.tag_item(Fruit.banana, ItemTag.FRUIT) + content.tag_item(Fruit.pineapple, ItemTag.FRUIT) + content.tag_item(Fruit.mango, ItemTag.FRUIT) + content.tag_item(Vegetable.taro_root, ItemTag.VEGETABLE) + content.tag_item(Mushroom.magma_cap, ItemTag.EDIBLE_MUSHROOM) + + +ginger_island_content_pack = GingerIslandContentPack( + "Ginger Island (Vanilla)", + weak_dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + # Foraging + Forageable.dragon_tooth: ( + ForagingSource(regions=(Region.volcano_floor_10,)), + ), + Forageable.ginger: ( + ForagingSource(regions=(Region.island_west,)), + ), + Mushroom.magma_cap: ( + ForagingSource(regions=(Region.volcano_floor_5,)), + ), + + # Fruit tree + Fruit.banana: (HarvestFruitTreeSource(sapling=Sapling.banana, seasons=(Season.summer,)),), + Fruit.mango: (HarvestFruitTreeSource(sapling=Sapling.mango, seasons=(Season.summer,)),), + + # Crop + Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),), + Fruit.pineapple: (HarvestCropSource(seed=Seed.pineapple, seasons=(Season.summer,)),), + + }, + shop_sources={ + Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),), + Seed.pineapple: (ShopSource(items_price=((1, Mushroom.magma_cap),), shop_region=Region.island_trader),), + Sapling.banana: (ShopSource(items_price=((5, Forageable.dragon_tooth),), shop_region=Region.island_trader),), + Sapling.mango: (ShopSource(items_price=((75, Fish.mussel_node),), shop_region=Region.island_trader),), + + # This one is 10 diamonds, should maybe add time? + Book.the_diamond_hunter: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop), + ), + Book.queen_of_sauce_cookbook: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2, other_requirements=(WalnutRequirement(100),)),), # Worst book ever + + }, + fishes=( + # TODO override region so no need to add inaccessible regions in logic + fish_data.blue_discus, + fish_data.lionfish, + fish_data.midnight_carp, + fish_data.pufferfish, + fish_data.stingray, + fish_data.super_cucumber, + fish_data.tilapia, + fish_data.tuna + ), + villagers=( + villagers_data.leo, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py new file mode 100644 index 000000000000..913fe4b8ad96 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -0,0 +1,389 @@ +from ..game_content import ContentPack +from ...data import villagers_data, fish_data +from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource +from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource +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 +from ...strings.fish_names import WaterItem +from ...strings.food_names import Beverage, Meal +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.fruit_tree_names import Sapling +from ...strings.generic_names import Generic +from ...strings.material_names import Material +from ...strings.region_names import Region, LogicRegion +from ...strings.season_names import Season +from ...strings.seed_names import Seed, TreeSeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial + +pelican_town = ContentPack( + "Pelican Town (Vanilla)", + harvest_sources={ + # Spring + Forageable.daffodil: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.town, Region.railroad)), + ), + Forageable.dandelion: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.forest, Region.railroad)), + ), + Forageable.leek: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.wild_horseradish: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.forest, Region.secret_woods)), + ), + Forageable.salmonberry: ( + SeasonalForagingSource(season=Season.spring, days=(15, 16, 17, 18), + regions=(Region.backwoods, Region.mountain, Region.town, Region.forest, Region.tunnel_entrance, Region.railroad)), + ), + Forageable.spring_onion: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.forest,)), + ), + + # Summer + Fruit.grape: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.spice_berry: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.forest, Region.railroad)), + ), + Forageable.sweet_pea: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.fiddlehead_fern: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.secret_woods,)), + ), + + # Fall + Forageable.blackberry: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.town, Region.forest, Region.railroad)), + SeasonalForagingSource(season=Season.fall, days=(8, 9, 10, 11), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.tunnel_entrance, + Region.railroad)), + ), + Forageable.hazelnut: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.wild_plum: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.mountain, Region.bus_stop, Region.railroad)), + ), + + # Winter + Forageable.crocus: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.secret_woods)), + ), + Forageable.crystal_fruit: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.holly: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.snow_yam: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad, + Region.secret_woods, Region.beach), + other_requirements=(ToolRequirement(Tool.hoe),)), + ), + Forageable.winter_root: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad, + Region.secret_woods, Region.beach), + other_requirements=(ToolRequirement(Tool.hoe),)), + ), + + # Mushrooms + Mushroom.common: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.secret_woods,)), + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.forest)), + ), + Mushroom.chanterelle: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.secret_woods,)), + ), + Mushroom.morel: ( + ForagingSource(seasons=(Season.spring, Season.fall), regions=(Region.secret_woods,)), + ), + Mushroom.red: ( + ForagingSource(seasons=(Season.summer, Season.fall), regions=(Region.secret_woods,)), + ), + + # Beach + WaterItem.coral: ( + ForagingSource(regions=(Region.tide_pools,)), + SeasonalForagingSource(season=Season.summer, days=(12, 13, 14), regions=(Region.beach,)), + ), + WaterItem.nautilus_shell: ( + ForagingSource(seasons=(Season.winter,), regions=(Region.beach,)), + ), + Forageable.rainbow_shell: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.beach,)), + ), + WaterItem.sea_urchin: ( + ForagingSource(regions=(Region.tide_pools,)), + ), + + Seed.mixed: ( + ForagingSource(seasons=(Season.spring, Season.summer, Season.fall,), regions=(Region.town, Region.farm, Region.forest)), + ), + + Seed.mixed_flower: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.town, Region.farm, Region.forest)), + ), + + # Books + Book.jack_be_nimble_jack_be_thick: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ArtifactSpotSource(amount=22),), # After 22 spots, there are 50.48% chances player received the book. + Book.woodys_secret: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=(Region.forest, Region.mountain), + other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), SkillRequirement(Skill.foraging, 5))),), + }, + shop_sources={ + # Saplings + Sapling.apple: (ShopSource(money_price=4000, shop_region=Region.pierre_store),), + Sapling.apricot: (ShopSource(money_price=2000, shop_region=Region.pierre_store),), + Sapling.cherry: (ShopSource(money_price=3400, shop_region=Region.pierre_store),), + Sapling.orange: (ShopSource(money_price=4000, shop_region=Region.pierre_store),), + Sapling.peach: (ShopSource(money_price=6000, shop_region=Region.pierre_store),), + Sapling.pomegranate: (ShopSource(money_price=6000, shop_region=Region.pierre_store),), + + # Crop seeds, assuming they are bought in season, otherwise price is different with missing stock list. + Seed.parsnip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.bean: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.cauliflower: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.potato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.tulip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.kale: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.jazz: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.garlic: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.rice: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + + Seed.melon: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.tomato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.blueberry: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.pepper: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.wheat: (ShopSource(money_price=10, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.radish: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.poppy: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.spangle: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.hops: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.corn: (ShopSource(money_price=150, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.sunflower: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.red_cabbage: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + + Seed.eggplant: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.pumpkin: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.bok_choy: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.yam: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.cranberry: (ShopSource(money_price=240, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.fairy: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.amaranth: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.grape: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.artichoke: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + + Seed.broccoli: (ShopSource(items_price=((5, Material.moss),), shop_region=LogicRegion.raccoon_shop),), + Seed.carrot: (ShopSource(items_price=((1, TreeSeed.maple),), shop_region=LogicRegion.raccoon_shop),), + Seed.powdermelon: (ShopSource(items_price=((2, TreeSeed.acorn),), shop_region=LogicRegion.raccoon_shop),), + Seed.summer_squash: (ShopSource(items_price=((15, Material.sap),), shop_region=LogicRegion.raccoon_shop),), + + Seed.strawberry: (ShopSource(money_price=100, shop_region=LogicRegion.egg_festival, seasons=(Season.spring,)),), + Seed.rare_seed: (ShopSource(money_price=1000, shop_region=LogicRegion.traveling_cart, seasons=(Season.spring, Season.summer)),), + + # Saloon + Beverage.beer: (ShopSource(money_price=400, shop_region=Region.saloon),), + Meal.salad: (ShopSource(money_price=220, shop_region=Region.saloon),), + Meal.bread: (ShopSource(money_price=100, shop_region=Region.saloon),), + Meal.spaghetti: (ShopSource(money_price=240, shop_region=Region.saloon),), + Meal.pizza: (ShopSource(money_price=600, shop_region=Region.saloon),), + Beverage.coffee: (ShopSource(money_price=300, shop_region=Region.saloon),), + + # Books + Book.animal_catalogue: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=5000, shop_region=Region.ranch),), + Book.book_of_mysteries: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + MysteryBoxSource(amount=38),), # After 38 boxes, there are 49.99% chances player received the book. + Book.dwarvish_safety_manual: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=4000, shop_region=LogicRegion.mines_dwarf_shop), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.friendship_101: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + PrizeMachineSource(amount=9), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.horse_the_book: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),), + Book.jack_be_nimble_jack_be_thick: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.jewels_of_the_sea: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + FishingTreasureChestSource(amount=21), # After 21 chests, there are 49.44% chances player received the book. + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.mapping_cave_systems: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + 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)), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.ol_slitherlegs: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),), + Book.price_catalogue: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),), + Book.the_alleyway_buffet: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=(Region.town,), + other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), ToolRequirement(Tool.pickaxe, ToolMaterial.iron))), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.the_art_o_crabbing: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + 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), + ArtifactTroveSource(amount=18), # After 18 troves, there is 49,88% chances player received the book. + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.raccoon_journal: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3), + ShopSource(items_price=((999, Material.fiber),), shop_region=LogicRegion.raccoon_shop),), + Book.way_of_the_wind_pt_1: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=15000, shop_region=LogicRegion.bookseller_2),), + Book.way_of_the_wind_pt_2: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=35000, shop_region=LogicRegion.bookseller_2, other_requirements=(BookRequirement(Book.way_of_the_wind_pt_1),)),), + Book.woodys_secret: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + + # Experience Books + Book.book_of_stars: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.bait_and_bobber: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.combat_quarterly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.mining_monthly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.stardew_valley_almanac: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.woodcutters_weekly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + }, + fishes=( + fish_data.albacore, + fish_data.anchovy, + fish_data.bream, + fish_data.bullhead, + fish_data.carp, + fish_data.catfish, + fish_data.chub, + fish_data.dorado, + fish_data.eel, + fish_data.flounder, + fish_data.goby, + fish_data.halibut, + fish_data.herring, + fish_data.largemouth_bass, + fish_data.lingcod, + fish_data.midnight_carp, # Ginger island override + fish_data.octopus, + fish_data.perch, + fish_data.pike, + fish_data.pufferfish, # Ginger island override + fish_data.rainbow_trout, + fish_data.red_mullet, + fish_data.red_snapper, + fish_data.salmon, + fish_data.sardine, + fish_data.sea_cucumber, + fish_data.shad, + fish_data.slimejack, + fish_data.smallmouth_bass, + fish_data.squid, + fish_data.sturgeon, + fish_data.sunfish, + fish_data.super_cucumber, # Ginger island override + fish_data.tiger_trout, + fish_data.tilapia, # Ginger island override + fish_data.tuna, # Ginger island override + fish_data.void_salmon, + fish_data.walleye, + fish_data.woodskip, + fish_data.blobfish, + fish_data.midnight_squid, + fish_data.spook_fish, + + # Legendaries + fish_data.angler, + fish_data.crimsonfish, + fish_data.glacierfish, + fish_data.legend, + fish_data.mutant_carp, + + # Crab pot + fish_data.clam, + fish_data.cockle, + fish_data.crab, + fish_data.crayfish, + fish_data.lobster, + fish_data.mussel, + fish_data.oyster, + fish_data.periwinkle, + fish_data.shrimp, + fish_data.snail, + ), + villagers=( + villagers_data.josh, + villagers_data.elliott, + villagers_data.harvey, + villagers_data.sam, + villagers_data.sebastian, + villagers_data.shane, + villagers_data.abigail, + villagers_data.emily, + villagers_data.haley, + villagers_data.leah, + villagers_data.maru, + villagers_data.penny, + villagers_data.caroline, + villagers_data.clint, + villagers_data.demetrius, + villagers_data.evelyn, + villagers_data.george, + villagers_data.gus, + villagers_data.jas, + villagers_data.jodi, + villagers_data.kent, + villagers_data.krobus, + villagers_data.lewis, + villagers_data.linus, + villagers_data.marnie, + villagers_data.pam, + villagers_data.pierre, + villagers_data.robin, + villagers_data.vincent, + villagers_data.willy, + villagers_data.wizard, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/qi_board.py b/worlds/stardew_valley/content/vanilla/qi_board.py new file mode 100644 index 000000000000..d859d3b16ff7 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/qi_board.py @@ -0,0 +1,36 @@ +from .ginger_island import ginger_island_content_pack as ginger_island_content_pack +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack, StardewContent +from ...data import fish_data +from ...data.game_item import GenericSource, ItemTag +from ...data.harvest import HarvestCropSource +from ...strings.crop_names import Fruit +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + + +class QiBoardContentPack(ContentPack): + def harvest_source_hook(self, content: StardewContent): + content.untag_item(Seed.qi_bean, ItemTag.CROPSANITY_SEED) + + +qi_board_content_pack = QiBoardContentPack( + "Qi Board (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ginger_island_content_pack.name, + ), + harvest_sources={ + # This one is a bit special, because it's only available during the special order, but it can be found from like, everywhere. + Seed.qi_bean: (GenericSource(regions=(Region.qi_walnut_room,)),), + Fruit.qi_fruit: (HarvestCropSource(seed=Seed.qi_bean),), + }, + fishes=( + fish_data.ms_angler, + fish_data.son_of_crimsonfish, + fish_data.glacierfish_jr, + fish_data.legend_ii, + fish_data.radioactive_carp, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/the_desert.py b/worlds/stardew_valley/content/vanilla/the_desert.py new file mode 100644 index 000000000000..a207e169ca46 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_desert.py @@ -0,0 +1,46 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data import fish_data, villagers_data +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.shop import ShopSource +from ...strings.crop_names import Fruit, Vegetable +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + +the_desert = ContentPack( + "The Desert (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + Forageable.cactus_fruit: ( + ForagingSource(regions=(Region.desert,)), + HarvestCropSource(seed=Seed.cactus, seasons=()) + ), + Forageable.coconut: ( + ForagingSource(regions=(Region.desert,)), + ), + Mushroom.purple: ( + ForagingSource(regions=(Region.skull_cavern_25,)), + ), + + Fruit.rhubarb: (HarvestCropSource(seed=Seed.rhubarb, seasons=(Season.spring,)),), + Fruit.starfruit: (HarvestCropSource(seed=Seed.starfruit, seasons=(Season.summer,)),), + Vegetable.beet: (HarvestCropSource(seed=Seed.beet, seasons=(Season.fall,)),), + }, + shop_sources={ + Seed.cactus: (ShopSource(money_price=150, shop_region=Region.oasis),), + Seed.rhubarb: (ShopSource(money_price=100, shop_region=Region.oasis, seasons=(Season.spring,)),), + Seed.starfruit: (ShopSource(money_price=400, shop_region=Region.oasis, seasons=(Season.summer,)),), + Seed.beet: (ShopSource(money_price=20, shop_region=Region.oasis, seasons=(Season.fall,)),), + }, + fishes=( + fish_data.sandfish, + fish_data.scorpion_carp, + ), + villagers=( + villagers_data.sandy, + ), +) diff --git a/worlds/stardew_valley/content/vanilla/the_farm.py b/worlds/stardew_valley/content/vanilla/the_farm.py new file mode 100644 index 000000000000..68d0bf10f6b8 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_farm.py @@ -0,0 +1,43 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data.harvest import FruitBatsSource, MushroomCaveSource +from ...strings.forageable_names import Forageable, Mushroom + +the_farm = ContentPack( + "The Farm (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + # Fruit cave + Forageable.blackberry: ( + FruitBatsSource(), + ), + Forageable.salmonberry: ( + FruitBatsSource(), + ), + Forageable.spice_berry: ( + FruitBatsSource(), + ), + Forageable.wild_plum: ( + FruitBatsSource(), + ), + + # Mushrooms + Mushroom.common: ( + MushroomCaveSource(), + ), + Mushroom.chanterelle: ( + MushroomCaveSource(), + ), + Mushroom.morel: ( + MushroomCaveSource(), + ), + Mushroom.purple: ( + MushroomCaveSource(), + ), + Mushroom.red: ( + MushroomCaveSource(), + ), + } +) diff --git a/worlds/stardew_valley/content/vanilla/the_mines.py b/worlds/stardew_valley/content/vanilla/the_mines.py new file mode 100644 index 000000000000..729b195f7b06 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_mines.py @@ -0,0 +1,35 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data import fish_data, villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import ToolRequirement +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.region_names import Region +from ...strings.tool_names import Tool + +the_mines = ContentPack( + "The Mines (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + Forageable.cave_carrot: ( + ForagingSource(regions=(Region.mines_floor_10,), other_requirements=(ToolRequirement(Tool.hoe),)), + ), + Mushroom.red: ( + ForagingSource(regions=(Region.mines_floor_95,)), + ), + Mushroom.purple: ( + ForagingSource(regions=(Region.mines_floor_95,)), + ) + }, + fishes=( + fish_data.ghostfish, + fish_data.ice_pip, + fish_data.lava_eel, + fish_data.stonefish, + ), + villagers=( + villagers_data.dwarf, + ), +) diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py index d14d9cfb8e97..e69de29bb2d1 100644 --- a/worlds/stardew_valley/data/__init__.py +++ b/worlds/stardew_valley/data/__init__.py @@ -1,2 +0,0 @@ -from .crops_data import CropItem, SeedItem, all_crops, all_purchasable_seeds -from .fish_data import FishItem, all_fish diff --git a/worlds/stardew_valley/data/artisan.py b/worlds/stardew_valley/data/artisan.py new file mode 100644 index 000000000000..90be5b1684f0 --- /dev/null +++ b/worlds/stardew_valley/data/artisan.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from .game_item import ItemSource + + +@dataclass(frozen=True, kw_only=True) +class MachineSource(ItemSource): + item: str # this should be optional (worm bin) + machine: str + # seasons diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 7e7a08c16b37..8b2e189c796e 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -1,17 +1,19 @@ from ..bundles.bundle import BundleTemplate, IslandBundleTemplate, DeepBundleTemplate, CurrencyBundleTemplate, MoneyBundleTemplate, FestivalBundleTemplate from ..bundles.bundle_item import BundleItem from ..bundles.bundle_room import BundleRoomTemplate +from ..content import content_packs +from ..content.vanilla.base import all_fruits, all_vegetables, all_edible_mushrooms from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood from ..strings.bundle_names import CCRoom, BundleName -from ..strings.craftable_names import Fishing, Craftable, Bomb +from ..strings.craftable_names import Fishing, Craftable, Bomb, Consumable, Lighting from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro -from ..strings.fish_names import Fish, WaterItem, Trash +from ..strings.fish_names import Fish, WaterItem, Trash, all_fish from ..strings.flower_names import Flower from ..strings.food_names import Beverage, Meal -from ..strings.forageable_names import Forageable +from ..strings.forageable_names import Forageable, Mushroom from ..strings.geode_names import Geode from ..strings.gift_names import Gift from ..strings.ingredient_names import Ingredient @@ -19,27 +21,27 @@ from ..strings.metal_names import MetalBar, Artifact, Fossil, Ore, Mineral from ..strings.monster_drop_names import Loot from ..strings.quality_names import ForageQuality, ArtisanQuality, FishQuality -from ..strings.seed_names import Seed +from ..strings.seed_names import Seed, TreeSeed wild_horseradish = BundleItem(Forageable.wild_horseradish) daffodil = BundleItem(Forageable.daffodil) leek = BundleItem(Forageable.leek) dandelion = BundleItem(Forageable.dandelion) -morel = BundleItem(Forageable.morel) -common_mushroom = BundleItem(Forageable.common_mushroom) +morel = BundleItem(Mushroom.morel) +common_mushroom = BundleItem(Mushroom.common) salmonberry = BundleItem(Forageable.salmonberry) spring_onion = BundleItem(Forageable.spring_onion) grape = BundleItem(Fruit.grape) spice_berry = BundleItem(Forageable.spice_berry) sweet_pea = BundleItem(Forageable.sweet_pea) -red_mushroom = BundleItem(Forageable.red_mushroom) +red_mushroom = BundleItem(Mushroom.red) fiddlehead_fern = BundleItem(Forageable.fiddlehead_fern) wild_plum = BundleItem(Forageable.wild_plum) hazelnut = BundleItem(Forageable.hazelnut) blackberry = BundleItem(Forageable.blackberry) -chanterelle = BundleItem(Forageable.chanterelle) +chanterelle = BundleItem(Mushroom.chanterelle) winter_root = BundleItem(Forageable.winter_root) crystal_fruit = BundleItem(Forageable.crystal_fruit) @@ -50,7 +52,7 @@ coconut = BundleItem(Forageable.coconut) cactus_fruit = BundleItem(Forageable.cactus_fruit) cave_carrot = BundleItem(Forageable.cave_carrot) -purple_mushroom = BundleItem(Forageable.purple_mushroom) +purple_mushroom = BundleItem(Mushroom.purple) maple_syrup = BundleItem(ArtisanGood.maple_syrup) oak_resin = BundleItem(ArtisanGood.oak_resin) pine_tar = BundleItem(ArtisanGood.pine_tar) @@ -62,13 +64,25 @@ cockle = BundleItem(Fish.cockle) mussel = BundleItem(Fish.mussel) oyster = BundleItem(Fish.oyster) -seaweed = BundleItem(WaterItem.seaweed) +seaweed = BundleItem(WaterItem.seaweed, can_have_quality=False) wood = BundleItem(Material.wood, 99) stone = BundleItem(Material.stone, 99) hardwood = BundleItem(Material.hardwood, 10) clay = BundleItem(Material.clay, 10) fiber = BundleItem(Material.fiber, 99) +moss = BundleItem(Material.moss, 10) + +mixed_seeds = BundleItem(Seed.mixed) +acorn = BundleItem(TreeSeed.acorn) +maple_seed = BundleItem(TreeSeed.maple) +pine_cone = BundleItem(TreeSeed.pine) +mahogany_seed = BundleItem(TreeSeed.mahogany) +mushroom_tree_seed = BundleItem(TreeSeed.mushroom, source=BundleItem.Sources.island) +mystic_tree_seed = BundleItem(TreeSeed.mystic, source=BundleItem.Sources.masteries) +mossy_seed = BundleItem(TreeSeed.mossy) + +strawberry_seeds = BundleItem(Seed.strawberry) blue_jazz = BundleItem(Flower.blue_jazz) cauliflower = BundleItem(Vegetable.cauliflower) @@ -106,8 +120,13 @@ red_cabbage = BundleItem(Vegetable.red_cabbage) starfruit = BundleItem(Fruit.starfruit) artichoke = BundleItem(Vegetable.artichoke) -pineapple = BundleItem(Fruit.pineapple, source=BundleItem.Sources.island) -taro_root = BundleItem(Vegetable.taro_root, source=BundleItem.Sources.island, ) +pineapple = BundleItem(Fruit.pineapple, source=BundleItem.Sources.content) +taro_root = BundleItem(Vegetable.taro_root, source=BundleItem.Sources.content) + +carrot = BundleItem(Vegetable.carrot) +summer_squash = BundleItem(Vegetable.summer_squash) +broccoli = BundleItem(Vegetable.broccoli) +powdermelon = BundleItem(Fruit.powdermelon) egg = BundleItem(AnimalProduct.egg) large_egg = BundleItem(AnimalProduct.large_egg) @@ -151,8 +170,8 @@ peach = BundleItem(Fruit.peach) pomegranate = BundleItem(Fruit.pomegranate) cherry = BundleItem(Fruit.cherry) -banana = BundleItem(Fruit.banana, source=BundleItem.Sources.island) -mango = BundleItem(Fruit.mango, source=BundleItem.Sources.island) +banana = BundleItem(Fruit.banana, source=BundleItem.Sources.content) +mango = BundleItem(Fruit.mango, source=BundleItem.Sources.content) basic_fertilizer = BundleItem(Fertilizer.basic, 100) quality_fertilizer = BundleItem(Fertilizer.quality, 20) @@ -300,6 +319,13 @@ rhubarb_pie = BundleItem(Meal.rhubarb_pie) shrimp_cocktail = BundleItem(Meal.shrimp_cocktail) pina_colada = BundleItem(Beverage.pina_colada, source=BundleItem.Sources.island) +stuffing = BundleItem(Meal.stuffing) +magic_rock_candy = BundleItem(Meal.magic_rock_candy) +spicy_eel = BundleItem(Meal.spicy_eel) +crab_cakes = BundleItem(Meal.crab_cakes) +eggplant_parmesan = BundleItem(Meal.eggplant_parmesan) +pumpkin_soup = BundleItem(Meal.pumpkin_soup) +lucky_lunch = BundleItem(Meal.lucky_lunch) green_algae = BundleItem(WaterItem.green_algae) white_algae = BundleItem(WaterItem.white_algae) @@ -370,6 +396,7 @@ spinner = BundleItem(Fishing.spinner) dressed_spinner = BundleItem(Fishing.dressed_spinner) trap_bobber = BundleItem(Fishing.trap_bobber) +sonar_bobber = BundleItem(Fishing.sonar_bobber) cork_bobber = BundleItem(Fishing.cork_bobber) lead_bobber = BundleItem(Fishing.lead_bobber) treasure_hunter = BundleItem(Fishing.treasure_hunter) @@ -377,18 +404,67 @@ curiosity_lure = BundleItem(Fishing.curiosity_lure) quality_bobber = BundleItem(Fishing.quality_bobber) bait = BundleItem(Fishing.bait, 100) +deluxe_bait = BundleItem(Fishing.deluxe_bait, 50) magnet = BundleItem(Fishing.magnet) -wild_bait = BundleItem(Fishing.wild_bait, 10) -magic_bait = BundleItem(Fishing.magic_bait, 5, source=BundleItem.Sources.island) +wild_bait = BundleItem(Fishing.wild_bait, 20) +magic_bait = BundleItem(Fishing.magic_bait, 10, source=BundleItem.Sources.island) pearl = BundleItem(Gift.pearl) +challenge_bait = BundleItem(Fishing.challenge_bait, 25, source=BundleItem.Sources.masteries) +targeted_bait = BundleItem(ArtisanGood.targeted_bait, 25, source=BundleItem.Sources.content) -ginger = BundleItem(Forageable.ginger, source=BundleItem.Sources.island) -magma_cap = BundleItem(Forageable.magma_cap, source=BundleItem.Sources.island) +ginger = BundleItem(Forageable.ginger, source=BundleItem.Sources.content) +magma_cap = BundleItem(Mushroom.magma_cap, source=BundleItem.Sources.content) wheat_flour = BundleItem(Ingredient.wheat_flour) sugar = BundleItem(Ingredient.sugar) vinegar = BundleItem(Ingredient.vinegar) +jack_o_lantern = BundleItem(Lighting.jack_o_lantern) +prize_ticket = BundleItem(Currency.prize_ticket) +mystery_box = BundleItem(Consumable.mystery_box) +gold_mystery_box = BundleItem(Consumable.gold_mystery_box, source=BundleItem.Sources.masteries) +calico_egg = BundleItem(Currency.calico_egg) + +raccoon_crab_pot_fish_items = [periwinkle.as_amount(5), snail.as_amount(5), crayfish.as_amount(5), mussel.as_amount(5), + oyster.as_amount(5), cockle.as_amount(5), clam.as_amount(5)] +raccoon_smoked_fish_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in + [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad, + Fish.rainbow_trout, Fish.tilapia, Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch]] +raccoon_fish_items_flat = [*raccoon_crab_pot_fish_items, *raccoon_smoked_fish_items] +raccoon_fish_items_deep = [raccoon_crab_pot_fish_items, raccoon_smoked_fish_items] +raccoon_fish_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_deep, 2, 2) +raccoon_fish_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_flat, 3, 2) + +all_specific_jellies = [BundleItem(ArtisanGood.jelly, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits] +all_specific_pickles = [BundleItem(ArtisanGood.pickles, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables] +all_specific_dried_fruits = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits], + BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)] +all_specific_juices = [BundleItem(ArtisanGood.juice, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables] +raccoon_artisan_items = [*all_specific_jellies, *all_specific_pickles, *all_specific_dried_fruits, *all_specific_juices] +raccoon_artisan_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 2, 2) +raccoon_artisan_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 3, 2) + +all_specific_dried_mushrooms = [BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms] +raccoon_food_items = [egg.as_amount(5), cave_carrot.as_amount(5), white_algae.as_amount(5)] +raccoon_food_items_vanilla = [all_specific_dried_mushrooms, raccoon_food_items] +raccoon_food_items_thematic = [*all_specific_dried_mushrooms, *raccoon_food_items, brown_egg.as_amount(5), large_egg.as_amount(2), large_brown_egg.as_amount(2), + green_algae.as_amount(10)] +raccoon_food_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_vanilla, 2, 2) +raccoon_food_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_thematic, 3, 2) + +raccoon_foraging_items = [moss, rusty_spoon, trash.as_amount(5), slime.as_amount(99), bat_wing.as_amount(10), geode.as_amount(8), + frozen_geode.as_amount(5), magma_geode.as_amount(3), coral.as_amount(4), sea_urchin.as_amount(2), bug_meat.as_amount(10), + diamond, topaz.as_amount(3), ghostfish.as_amount(3)] +raccoon_foraging_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 2, 2) +raccoon_foraging_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 3, 2) + +raccoon_bundles_vanilla = [raccoon_fish_bundle_vanilla, raccoon_artisan_bundle_vanilla, raccoon_food_bundle_vanilla, raccoon_foraging_bundle_vanilla] +raccoon_bundles_thematic = [raccoon_fish_bundle_thematic, raccoon_artisan_bundle_thematic, raccoon_food_bundle_thematic, raccoon_foraging_bundle_thematic] +raccoon_bundles_remixed = raccoon_bundles_thematic +raccoon_vanilla = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_vanilla, 8) +raccoon_thematic = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_thematic, 8) +raccoon_remixed = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_remixed, 8) + # Crafts Room spring_foraging_items_vanilla = [wild_horseradish, daffodil, leek, dandelion] spring_foraging_items_thematic = [*spring_foraging_items_vanilla, spring_onion, salmonberry, morel] @@ -436,42 +512,50 @@ sticky_items = [sap.as_amount(500), sap.as_amount(500)] sticky_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.sticky, sticky_items, 1, 1) +forest_items = [moss, fiber.as_amount(200), acorn.as_amount(10), maple_seed.as_amount(10), pine_cone.as_amount(10), mahogany_seed, + mushroom_tree_seed, mossy_seed.as_amount(5), mystic_tree_seed] +forest_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.forest, forest_items, 4, 2) + wild_medicine_items = [item.as_amount(5) for item in [purple_mushroom, fiddlehead_fern, white_algae, hops, blackberry, dandelion]] wild_medicine_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.wild_medicine, wild_medicine_items, 4, 3) -quality_foraging_items = sorted({item.as_quality(ForageQuality.gold).as_amount(1) +quality_foraging_items = sorted({item.as_quality(ForageQuality.gold).as_amount(3) for item in [*spring_foraging_items_thematic, *summer_foraging_items_thematic, *fall_foraging_items_thematic, - *winter_foraging_items_thematic, *beach_foraging_items, *desert_foraging_items, magma_cap]}) + *winter_foraging_items_thematic, *beach_foraging_items, *desert_foraging_items, magma_cap] if item.can_have_quality}) quality_foraging_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.quality_foraging, quality_foraging_items, 4, 3) +green_rain_items = [moss.as_amount(200), fiber.as_amount(200), mossy_seed.as_amount(20), fiddlehead_fern.as_amount(10)] +green_rain_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.green_rain, green_rain_items, 4, 3) + crafts_room_bundles_vanilla = [spring_foraging_bundle_vanilla, summer_foraging_bundle_vanilla, fall_foraging_bundle_vanilla, winter_foraging_bundle_vanilla, construction_bundle_vanilla, exotic_foraging_bundle_vanilla] crafts_room_bundles_thematic = [spring_foraging_bundle_thematic, summer_foraging_bundle_thematic, fall_foraging_bundle_thematic, winter_foraging_bundle_thematic, construction_bundle_thematic, exotic_foraging_bundle_thematic] crafts_room_bundles_remixed = [*crafts_room_bundles_thematic, beach_foraging_bundle, mines_foraging_bundle, desert_foraging_bundle, - island_foraging_bundle, sticky_bundle, wild_medicine_bundle, quality_foraging_bundle] + island_foraging_bundle, sticky_bundle, forest_bundle, wild_medicine_bundle, quality_foraging_bundle, green_rain_bundle] crafts_room_vanilla = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_vanilla, 6) crafts_room_thematic = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_thematic, 6) crafts_room_remixed = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_remixed, 6) # Pantry spring_crops_items_vanilla = [parsnip, green_bean, cauliflower, potato] -spring_crops_items_thematic = [*spring_crops_items_vanilla, blue_jazz, coffee_bean, garlic, kale, rhubarb, strawberry, tulip, unmilled_rice] +spring_crops_items_thematic = [*spring_crops_items_vanilla, blue_jazz, coffee_bean, garlic, kale, rhubarb, strawberry, tulip, unmilled_rice, carrot] spring_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.spring_crops, spring_crops_items_vanilla, 4, 4) spring_crops_bundle_thematic = BundleTemplate.extend_from(spring_crops_bundle_vanilla, spring_crops_items_thematic) summer_crops_items_vanilla = [tomato, hot_pepper, blueberry, melon] -summer_crops_items_thematic = [*summer_crops_items_vanilla, corn, hops, poppy, radish, red_cabbage, starfruit, summer_spangle, sunflower, wheat] +summer_crops_items_thematic = [*summer_crops_items_vanilla, corn, hops, poppy, radish, red_cabbage, starfruit, summer_spangle, sunflower, wheat, summer_squash] summer_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.summer_crops, summer_crops_items_vanilla, 4, 4) summer_crops_bundle_thematic = BundleTemplate.extend_from(summer_crops_bundle_vanilla, summer_crops_items_thematic) fall_crops_items_vanilla = [corn, eggplant, pumpkin, yam] -fall_crops_items_thematic = [*fall_crops_items_vanilla, amaranth, artichoke, beet, bok_choy, cranberries, fairy_rose, grape, sunflower, wheat, sweet_gem_berry] +fall_crops_items_thematic = [*fall_crops_items_vanilla, amaranth, artichoke, beet, bok_choy, cranberries, fairy_rose, grape, + sunflower, wheat, sweet_gem_berry, broccoli] fall_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.fall_crops, fall_crops_items_vanilla, 4, 4) fall_crops_bundle_thematic = BundleTemplate.extend_from(fall_crops_bundle_vanilla, fall_crops_items_thematic) -all_crops_items = sorted({*spring_crops_items_thematic, *summer_crops_items_thematic, *fall_crops_items_thematic}) +all_crops_items = sorted({*spring_crops_items_thematic, *summer_crops_items_thematic, *fall_crops_items_thematic, powdermelon}) quality_crops_items_vanilla = [item.as_quality_crop() for item in [parsnip, melon, pumpkin, corn]] quality_crops_items_thematic = [item.as_quality_crop() for item in all_crops_items] @@ -492,7 +576,8 @@ rare_crops_items = [ancient_fruit, sweet_gem_berry] rare_crops_bundle = BundleTemplate(CCRoom.pantry, BundleName.rare_crops, rare_crops_items, 2, 2) -fish_farmer_items = [roe.as_amount(15), aged_roe.as_amount(15), squid_ink] +# all_specific_roes = [BundleItem(AnimalProduct.roe, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fish] +fish_farmer_items = [roe.as_amount(15), aged_roe.as_amount(5), squid_ink, caviar.as_amount(5)] fish_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.fish_farmer, fish_farmer_items, 3, 2) garden_items = [tulip, blue_jazz, summer_spangle, sunflower, fairy_rose, poppy, bouquet] @@ -516,12 +601,20 @@ purple_slime_egg, green_slime_egg, tiger_slime_egg] slime_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.slime_farmer, slime_farmer_items, 4, 3) +sommelier_items = [BundleItem(ArtisanGood.wine, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits] +sommelier_bundle = BundleTemplate(CCRoom.pantry, BundleName.sommelier, sommelier_items, 6, 3) + +dry_items = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits], + *[BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms], + BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)] +dry_bundle = BundleTemplate(CCRoom.pantry, BundleName.dry, dry_items, 6, 3) + pantry_bundles_vanilla = [spring_crops_bundle_vanilla, summer_crops_bundle_vanilla, fall_crops_bundle_vanilla, quality_crops_bundle_vanilla, animal_bundle_vanilla, artisan_bundle_vanilla] pantry_bundles_thematic = [spring_crops_bundle_thematic, summer_crops_bundle_thematic, fall_crops_bundle_thematic, quality_crops_bundle_thematic, animal_bundle_thematic, artisan_bundle_thematic] pantry_bundles_remixed = [*pantry_bundles_thematic, rare_crops_bundle, fish_farmer_bundle, garden_bundle, - brewer_bundle, orchard_bundle, island_crops_bundle, agronomist_bundle, slime_farmer_bundle] + brewer_bundle, orchard_bundle, island_crops_bundle, agronomist_bundle, slime_farmer_bundle, sommelier_bundle, dry_bundle] pantry_vanilla = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_vanilla, 6) pantry_thematic = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_thematic, 6) pantry_remixed = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_remixed, 6) @@ -579,8 +672,11 @@ rain_fish_items = [red_snapper, shad, catfish, eel, walleye] rain_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.rain_fish, rain_fish_items, 3, 3) -quality_fish_items = sorted({item.as_quality(FishQuality.gold) for item in [*river_fish_items_thematic, *lake_fish_items_thematic, *ocean_fish_items_thematic]}) -quality_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.quality_fish, quality_fish_items, 4, 4) +quality_fish_items = sorted({ + item.as_quality(FishQuality.gold).as_amount(2) + for item in [*river_fish_items_thematic, *lake_fish_items_thematic, *ocean_fish_items_thematic] +}) +quality_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.quality_fish, quality_fish_items, 4, 3) master_fisher_items = [lava_eel, scorpion_carp, octopus, blobfish, lingcod, ice_pip, super_cucumber, stingray, void_salmon, pufferfish] master_fisher_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.master_fisher, master_fisher_items, 4, 2) @@ -591,21 +687,31 @@ island_fish_items = [lionfish, blue_discus, stingray] island_fish_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.island_fish, island_fish_items, 3, 3) -tackle_items = [spinner, dressed_spinner, trap_bobber, cork_bobber, lead_bobber, treasure_hunter, barbed_hook, curiosity_lure, quality_bobber] -tackle_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.tackle, tackle_items, 3, 2) +tackle_items = [spinner, dressed_spinner, trap_bobber, sonar_bobber, cork_bobber, lead_bobber, treasure_hunter, barbed_hook, curiosity_lure, quality_bobber] +tackle_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.tackle, tackle_items, 3, 2) -bait_items = [bait, magnet, wild_bait, magic_bait] -bait_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.bait, bait_items, 2, 2) +bait_items = [bait, magnet, wild_bait, magic_bait, challenge_bait, deluxe_bait, targeted_bait] +bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.bait, bait_items, 3, 2) + +# This bundle could change based on content packs, once the fish are properly in it. Until then, I'm not sure how, so pelican town only +specific_bait_items = [BundleItem(ArtisanGood.targeted_bait, flavor=fish.name).as_amount(10) for fish in content_packs.pelican_town.fishes] +specific_bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.specific_bait, specific_bait_items, 6, 3) deep_fishing_items = [blobfish, spook_fish, midnight_squid, sea_cucumber, super_cucumber, octopus, pearl, seaweed] deep_fishing_bundle = FestivalBundleTemplate(CCRoom.fish_tank, BundleName.deep_fishing, deep_fishing_items, 4, 3) +smokeable_fish = [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad, Fish.rainbow_trout, Fish.tilapia, + Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch] +fish_smoker_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in smokeable_fish] +fish_smoker_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.fish_smoker, fish_smoker_items, 6, 3) + fish_tank_bundles_vanilla = [river_fish_bundle_vanilla, lake_fish_bundle_vanilla, ocean_fish_bundle_vanilla, night_fish_bundle_vanilla, crab_pot_bundle_vanilla, specialty_fish_bundle_vanilla] fish_tank_bundles_thematic = [river_fish_bundle_thematic, lake_fish_bundle_thematic, ocean_fish_bundle_thematic, night_fish_bundle_thematic, crab_pot_bundle_thematic, specialty_fish_bundle_thematic] fish_tank_bundles_remixed = [*fish_tank_bundles_thematic, spring_fish_bundle, summer_fish_bundle, fall_fish_bundle, winter_fish_bundle, trash_bundle, - rain_fish_bundle, quality_fish_bundle, master_fisher_bundle, legendary_fish_bundle, tackle_bundle, bait_bundle] + rain_fish_bundle, quality_fish_bundle, master_fisher_bundle, legendary_fish_bundle, tackle_bundle, bait_bundle, + specific_bait_bundle, deep_fishing_bundle, fish_smoker_bundle] # In Remixed, the trash items are in the recycling bundle, so we don't use the thematic version of the crab pot bundle that added trash items to it fish_tank_bundles_remixed.remove(crab_pot_bundle_thematic) @@ -670,12 +776,12 @@ chef_bundle_thematic = BundleTemplate.extend_from(chef_bundle_vanilla, chef_items_thematic) dye_items_vanilla = [red_mushroom, sea_urchin, sunflower, duck_feather, aquamarine, red_cabbage] -dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] +dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip, red_mushroom] dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root] -dye_yellow_items = [corn, parsnip, summer_spangle, sunflower] -dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean] -dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit] -dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea] +dye_yellow_items = [corn, parsnip, summer_spangle, sunflower, starfruit] +dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean, cactus_fruit, duck_feather, dinosaur_egg] +dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit, aquamarine] +dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea, iridium_bar, sea_urchin, amaranth] dye_items_thematic = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items] dye_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_vanilla, 6, 6) dye_bundle_thematic = DeepBundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_thematic, 6, 6) @@ -710,12 +816,31 @@ chocolate_cake, pancakes, rhubarb_pie] home_cook_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.home_cook, home_cook_items, 3, 3) +helper_items = [prize_ticket, mystery_box.as_amount(5), gold_mystery_box] +helper_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.helper, helper_items, 2, 2) + +spirit_eve_items = [jack_o_lantern, corn.as_amount(10), bat_wing.as_amount(10)] +spirit_eve_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.spirit_eve, spirit_eve_items, 3, 3) + +winter_star_items = [holly.as_amount(5), plum_pudding, stuffing, powdermelon.as_amount(5)] +winter_star_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.winter_star, winter_star_items, 2, 2) + bartender_items = [shrimp_cocktail, triple_shot_espresso, ginger_ale, cranberry_candy, beer, pale_ale, pina_colada] bartender_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.bartender, bartender_items, 3, 3) +calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200), + magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50), + strawberry_seeds.as_amount(20), + spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5), + pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),] +calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2) + +raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4) + bulletin_board_bundles_vanilla = [chef_bundle_vanilla, dye_bundle_vanilla, field_research_bundle_vanilla, fodder_bundle_vanilla, enchanter_bundle_vanilla] bulletin_board_bundles_thematic = [chef_bundle_thematic, dye_bundle_thematic, field_research_bundle_thematic, fodder_bundle_thematic, enchanter_bundle_thematic] -bulletin_board_bundles_remixed = [*bulletin_board_bundles_thematic, children_bundle, forager_bundle, home_cook_bundle, bartender_bundle] +bulletin_board_bundles_remixed = [*bulletin_board_bundles_thematic, children_bundle, forager_bundle, home_cook_bundle, + helper_bundle, spirit_eve_bundle, winter_star_bundle, bartender_bundle, calico_bundle, raccoon_bundle] bulletin_board_vanilla = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_vanilla, 5) bulletin_board_thematic = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_thematic, 5) bulletin_board_remixed = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_remixed, 5) @@ -738,16 +863,15 @@ abandoned_joja_mart_thematic = BundleRoomTemplate(CCRoom.abandoned_joja_mart, abandoned_joja_mart_bundles_thematic, 1) abandoned_joja_mart_remixed = abandoned_joja_mart_thematic -# Make thematic with other currencies vault_2500_gold = BundleItem.money_bundle(2500) vault_5000_gold = BundleItem.money_bundle(5000) vault_10000_gold = BundleItem.money_bundle(10000) vault_25000_gold = BundleItem.money_bundle(25000) -vault_2500_bundle = MoneyBundleTemplate(CCRoom.vault, vault_2500_gold) -vault_5000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_5000_gold) -vault_10000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_10000_gold) -vault_25000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_25000_gold) +vault_2500_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_2500, vault_2500_gold) +vault_5000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_5000, vault_5000_gold) +vault_10000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_10000, vault_10000_gold) +vault_25000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_25000, vault_25000_gold) vault_gambler_items = BundleItem(Currency.qi_coin, 10000) vault_gambler_bundle = CurrencyBundleTemplate(CCRoom.vault, BundleName.gambler, vault_gambler_items) @@ -768,9 +892,14 @@ vault_thematic = BundleRoomTemplate(CCRoom.vault, vault_bundles_thematic, 4) vault_remixed = BundleRoomTemplate(CCRoom.vault, vault_bundles_remixed, 4) +all_cc_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed, + *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed] +community_center_remixed_anywhere = BundleRoomTemplate("Community Center", all_cc_remixed_bundles, 26) + all_bundle_items_except_money = [] all_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed, - *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed, missing_bundle_thematic] + *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed, missing_bundle_thematic, + *raccoon_bundles_remixed] for bundle in all_remixed_bundles: all_bundle_items_except_money.extend(bundle.items) diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index bfb2d25ec6b8..713db4732075 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -1,25 +1,28 @@ from typing import Dict, List, Optional -from ..mods.mod_data import ModNames from .recipe_source import RecipeSource, StarterSource, QueenOfSauceSource, ShopSource, SkillSource, FriendshipSource, ShopTradeSource, CutsceneSource, \ - ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource + ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource, SkillCraftsanitySource +from ..mods.mod_data import ModNames +from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood -from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor, Fishing, Ring, Consumable, Edible, Lighting, Storage, Furniture, Sign, Craftable, \ - ModEdible, ModCraftable, ModMachine, ModFloor, ModConsumable +from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor, Fishing, Ring, Consumable, Edible, Lighting, Storage, Furniture, Sign, \ + Craftable, \ + ModEdible, ModCraftable, ModMachine, ModFloor, ModConsumable, Statue from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro -from ..strings.fish_names import Fish, WaterItem +from ..strings.fish_names import Fish, WaterItem, ModTrash from ..strings.flower_names import Flower from ..strings.food_names import Meal -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable +from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom +from ..strings.gift_names import Gift from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine from ..strings.material_names import Material from ..strings.metal_names import Ore, MetalBar, Fossil, Artifact, Mineral, ModFossil -from ..strings.monster_drop_names import Loot +from ..strings.monster_drop_names import Loot, ModLoot from ..strings.quest_names import Quest -from ..strings.region_names import Region, SVERegion +from ..strings.region_names import Region, SVERegion, LogicRegion from ..strings.seed_names import Seed, TreeSeed from ..strings.skill_names import Skill, ModSkill from ..strings.special_order_names import SpecialOrder @@ -61,6 +64,16 @@ def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], return create_recipe(name, ingredients, source, mod_name) +def skill_craftsanity_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: + source = SkillCraftsanitySource(skill, level) + return create_recipe(name, ingredients, source, mod_name) + + +def mastery_recipe(name: str, skill: str, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: + source = MasterySource(skill) + return create_recipe(name, ingredients, source, mod_name) + + def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: source = ShopSource(region, price) return create_recipe(name, ingredients, source, mod_name) @@ -133,27 +146,37 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, cheese_press = skill_recipe(Machine.cheese_press, Skill.farming, 6, {Material.wood: 45, Material.stone: 45, Material.hardwood: 10, MetalBar.copper: 1}) keg = skill_recipe(Machine.keg, Skill.farming, 8, {Material.wood: 30, MetalBar.copper: 1, MetalBar.iron: 1, ArtisanGood.oak_resin: 1}) loom = skill_recipe(Machine.loom, Skill.farming, 7, {Material.wood: 60, Material.fiber: 30, ArtisanGood.pine_tar: 1}) -mayonnaise_machine = skill_recipe(Machine.mayonnaise_machine, Skill.farming, 2, {Material.wood: 15, Material.stone: 15, Mineral.earth_crystal: 10, MetalBar.copper: 1}) +mayonnaise_machine = skill_recipe(Machine.mayonnaise_machine, Skill.farming, 2, + {Material.wood: 15, Material.stone: 15, Mineral.earth_crystal: 10, MetalBar.copper: 1}) oil_maker = skill_recipe(Machine.oil_maker, Skill.farming, 8, {Loot.slime: 50, Material.hardwood: 20, MetalBar.gold: 1}) preserves_jar = skill_recipe(Machine.preserves_jar, Skill.farming, 4, {Material.wood: 50, Material.stone: 40, Material.coal: 8}) +fish_smoker = shop_recipe(Machine.fish_smoker, Region.fish_shop, 10000, + {Material.hardwood: 10, WaterItem.sea_jelly: 1, WaterItem.river_jelly: 1, WaterItem.cave_jelly: 1}) +dehydrator = shop_recipe(Machine.dehydrator, Region.pierre_store, 10000, {Material.wood: 30, Material.clay: 2, Mineral.fire_quartz: 1}) basic_fertilizer = skill_recipe(Fertilizer.basic, Skill.farming, 1, {Material.sap: 2}) -quality_fertilizer = skill_recipe(Fertilizer.quality, Skill.farming, 9, {Material.sap: 2, Fish.any: 1}) + +quality_fertilizer = skill_recipe(Fertilizer.quality, Skill.farming, 9, {Material.sap: 4, Fish.any: 1}) deluxe_fertilizer = ap_recipe(Fertilizer.deluxe, {MetalBar.iridium: 1, Material.sap: 40}) -basic_speed_gro = skill_recipe(SpeedGro.basic, Skill.farming, 3, {ArtisanGood.pine_tar: 1, Fish.clam: 1}) -deluxe_speed_gro = skill_recipe(SpeedGro.deluxe, Skill.farming, 8, {ArtisanGood.oak_resin: 1, WaterItem.coral: 1}) + +basic_speed_gro = skill_recipe(SpeedGro.basic, Skill.farming, 3, {ArtisanGood.pine_tar: 1, Material.moss: 5}) +deluxe_speed_gro = skill_recipe(SpeedGro.deluxe, Skill.farming, 8, {ArtisanGood.oak_resin: 1, Fossil.bone_fragment: 5}) hyper_speed_gro = ap_recipe(SpeedGro.hyper, {Ore.radioactive: 1, Fossil.bone_fragment: 3, Loot.solar_essence: 1}) basic_retaining_soil = skill_recipe(RetainingSoil.basic, Skill.farming, 4, {Material.stone: 2}) quality_retaining_soil = skill_recipe(RetainingSoil.quality, Skill.farming, 7, {Material.stone: 3, Material.clay: 1}) -deluxe_retaining_soil = shop_trade_recipe(RetainingSoil.deluxe, Region.island_trader, Currency.cinder_shard, 50, {Material.stone: 5, Material.fiber: 3, Material.clay: 1}) +deluxe_retaining_soil = shop_trade_recipe(RetainingSoil.deluxe, Region.island_trader, Currency.cinder_shard, 50, + {Material.stone: 5, Material.fiber: 3, Material.clay: 1}) tree_fertilizer = skill_recipe(Fertilizer.tree, Skill.foraging, 7, {Material.fiber: 5, Material.stone: 5}) -spring_seeds = skill_recipe(WildSeeds.spring, Skill.foraging, 1, {Forageable.wild_horseradish: 1, Forageable.daffodil: 1, Forageable.leek: 1, Forageable.dandelion: 1}) +spring_seeds = skill_recipe(WildSeeds.spring, Skill.foraging, 1, + {Forageable.wild_horseradish: 1, Forageable.daffodil: 1, Forageable.leek: 1, Forageable.dandelion: 1}) summer_seeds = skill_recipe(WildSeeds.summer, Skill.foraging, 4, {Forageable.spice_berry: 1, Fruit.grape: 1, Forageable.sweet_pea: 1}) -fall_seeds = skill_recipe(WildSeeds.fall, Skill.foraging, 6, {Forageable.common_mushroom: 1, Forageable.wild_plum: 1, Forageable.hazelnut: 1, Forageable.blackberry: 1}) -winter_seeds = skill_recipe(WildSeeds.winter, Skill.foraging, 7, {Forageable.winter_root: 1, Forageable.crystal_fruit: 1, Forageable.snow_yam: 1, Forageable.crocus: 1}) +fall_seeds = skill_recipe(WildSeeds.fall, Skill.foraging, 6, {Mushroom.common: 1, Forageable.wild_plum: 1, Forageable.hazelnut: 1, Forageable.blackberry: 1}) +winter_seeds = skill_recipe(WildSeeds.winter, Skill.foraging, 7, + {Forageable.winter_root: 1, Forageable.crystal_fruit: 1, Forageable.snow_yam: 1, Forageable.crocus: 1}) ancient_seeds = ap_recipe(WildSeeds.ancient, {Artifact.ancient_seed: 1}) grass_starter = shop_recipe(WildSeeds.grass_starter, Region.pierre_store, 1000, {Material.fiber: 10}) +blue_grass_starter = ap_recipe(WildSeeds.blue_grass_starter, {Material.fiber: 25, Material.moss: 10, ArtisanGood.mystic_syrup: 1}) for wild_seeds in [WildSeeds.spring, WildSeeds.summer, WildSeeds.fall, WildSeeds.winter]: tea_sapling = cutscene_recipe(WildSeeds.tea_sapling, Region.sunroom, NPC.caroline, 2, {wild_seeds: 2, Material.fiber: 5, Material.wood: 5}) fiber_seeds = special_order_recipe(WildSeeds.fiber, SpecialOrder.community_cleanup, {Seed.mixed: 1, Material.sap: 5, Material.clay: 1}) @@ -161,7 +184,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, wood_floor = shop_recipe(Floor.wood, Region.carpenter, 100, {Material.wood: 1}) rustic_floor = shop_recipe(Floor.rustic, Region.carpenter, 200, {Material.wood: 1}) straw_floor = shop_recipe(Floor.straw, Region.carpenter, 200, {Material.wood: 1, Material.fiber: 1}) -weathered_floor = shop_recipe(Floor.weathered, Region.mines_dwarf_shop, 500, {Material.wood: 1}) +weathered_floor = shop_recipe(Floor.weathered, LogicRegion.mines_dwarf_shop, 500, {Material.wood: 1}) crystal_floor = shop_recipe(Floor.crystal, Region.sewer, 500, {MetalBar.quartz: 1}) stone_floor = shop_recipe(Floor.stone, Region.carpenter, 100, {Material.stone: 1}) stone_walkway_floor = shop_recipe(Floor.stone_walkway, Region.carpenter, 200, {Material.stone: 1}) @@ -174,6 +197,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, spinner = skill_recipe(Fishing.spinner, Skill.fishing, 6, {MetalBar.iron: 2}) trap_bobber = skill_recipe(Fishing.trap_bobber, Skill.fishing, 6, {MetalBar.copper: 1, Material.sap: 10}) +sonar_bobber = skill_recipe(Fishing.sonar_bobber, Skill.fishing, 6, {MetalBar.iron: 1, MetalBar.quartz: 2}) cork_bobber = skill_recipe(Fishing.cork_bobber, Skill.fishing, 7, {Material.wood: 10, Material.hardwood: 5, Loot.slime: 10}) quality_bobber = special_order_recipe(Fishing.quality_bobber, SpecialOrder.juicy_bugs_wanted, {MetalBar.copper: 1, Material.sap: 20, Loot.solar_essence: 5}) treasure_hunter = skill_recipe(Fishing.treasure_hunter, Skill.fishing, 7, {MetalBar.gold: 2}) @@ -181,6 +205,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, barbed_hook = skill_recipe(Fishing.barbed_hook, Skill.fishing, 8, {MetalBar.copper: 1, MetalBar.iron: 1, MetalBar.gold: 1}) magnet = skill_recipe(Fishing.magnet, Skill.fishing, 9, {MetalBar.iron: 1}) bait = skill_recipe(Fishing.bait, Skill.fishing, 2, {Loot.bug_meat: 1}) +deluxe_bait = skill_recipe(Fishing.deluxe_bait, Skill.fishing, 4, {Fishing.bait: 5, Material.moss: 2}) wild_bait = cutscene_recipe(Fishing.wild_bait, Region.tent, NPC.linus, 4, {Material.fiber: 10, Loot.bug_meat: 5, Loot.slime: 5}) magic_bait = ap_recipe(Fishing.magic_bait, {Ore.radioactive: 1, Loot.bug_meat: 3}) crab_pot = skill_recipe(Machine.crab_pot, Skill.fishing, 3, {Material.wood: 40, MetalBar.iron: 3}) @@ -191,11 +216,11 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, thorns_ring = skill_recipe(Ring.thorns_ring, Skill.combat, 7, {Fossil.bone_fragment: 50, Material.stone: 50, MetalBar.gold: 1}) glowstone_ring = skill_recipe(Ring.glowstone_ring, Skill.mining, 4, {Loot.solar_essence: 5, MetalBar.iron: 5}) iridium_band = skill_recipe(Ring.iridium_band, Skill.combat, 9, {MetalBar.iridium: 5, Loot.solar_essence: 50, Loot.void_essence: 50}) -wedding_ring = shop_recipe(Ring.wedding_ring, Region.traveling_cart, 500, {MetalBar.iridium: 5, Mineral.prismatic_shard: 1}) +wedding_ring = shop_recipe(Ring.wedding_ring, LogicRegion.traveling_cart, 500, {MetalBar.iridium: 5, Mineral.prismatic_shard: 1}) field_snack = skill_recipe(Edible.field_snack, Skill.foraging, 1, {TreeSeed.acorn: 1, TreeSeed.maple: 1, TreeSeed.pine: 1}) bug_steak = skill_recipe(Edible.bug_steak, Skill.combat, 1, {Loot.bug_meat: 10}) -life_elixir = skill_recipe(Edible.life_elixir, Skill.combat, 2, {Forageable.red_mushroom: 1, Forageable.purple_mushroom: 1, Forageable.morel: 1, Forageable.chanterelle: 1}) +life_elixir = skill_recipe(Edible.life_elixir, Skill.combat, 2, {Mushroom.red: 1, Mushroom.purple: 1, Mushroom.morel: 1, Mushroom.chanterelle: 1}) oil_of_garlic = skill_recipe(Edible.oil_of_garlic, Skill.combat, 6, {Vegetable.garlic: 10, Ingredient.oil: 1}) monster_musk = special_order_recipe(Consumable.monster_musk, SpecialOrder.prismatic_jelly, {Loot.bat_wing: 30, Loot.slime: 30}) @@ -203,8 +228,10 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, warp_totem_beach = skill_recipe(Consumable.warp_totem_beach, Skill.foraging, 6, {Material.hardwood: 1, WaterItem.coral: 2, Material.fiber: 10}) warp_totem_mountains = skill_recipe(Consumable.warp_totem_mountains, Skill.foraging, 7, {Material.hardwood: 1, MetalBar.iron: 1, Material.stone: 25}) warp_totem_farm = skill_recipe(Consumable.warp_totem_farm, Skill.foraging, 8, {Material.hardwood: 1, ArtisanGood.honey: 1, Material.fiber: 20}) -warp_totem_desert = shop_trade_recipe(Consumable.warp_totem_desert, Region.desert, MetalBar.iridium, 10, {Material.hardwood: 2, Forageable.coconut: 1, Ore.iridium: 4}) -warp_totem_island = shop_recipe(Consumable.warp_totem_island, Region.volcano_dwarf_shop, 10000, {Material.hardwood: 5, Forageable.dragon_tooth: 1, Forageable.ginger: 1}) +warp_totem_desert = shop_trade_recipe(Consumable.warp_totem_desert, Region.desert, MetalBar.iridium, 10, + {Material.hardwood: 2, Forageable.coconut: 1, Ore.iridium: 4}) +warp_totem_island = shop_recipe(Consumable.warp_totem_island, Region.volcano_dwarf_shop, 10000, + {Material.hardwood: 5, Forageable.dragon_tooth: 1, Forageable.ginger: 1}) rain_totem = skill_recipe(Consumable.rain_totem, Skill.foraging, 9, {Material.hardwood: 1, ArtisanGood.truffle_oil: 1, ArtisanGood.pine_tar: 5}) torch = starter_recipe(Lighting.torch, {Material.wood: 1, Material.sap: 2}) @@ -219,13 +246,19 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, marble_brazier = shop_recipe(Lighting.marble_brazier, Region.carpenter, 5000, {Mineral.marble: 1, Mineral.aquamarine: 1, Material.stone: 100}) wood_lamp_post = shop_recipe(Lighting.wood_lamp_post, Region.carpenter, 500, {Material.wood: 50, ArtisanGood.battery_pack: 1}) iron_lamp_post = shop_recipe(Lighting.iron_lamp_post, Region.carpenter, 1000, {MetalBar.iron: 1, ArtisanGood.battery_pack: 1}) -jack_o_lantern = festival_shop_recipe(Lighting.jack_o_lantern, Region.spirit_eve, 2000, {Vegetable.pumpkin: 1, Lighting.torch: 1}) +jack_o_lantern = festival_shop_recipe(Lighting.jack_o_lantern, LogicRegion.spirit_eve, 2000, {Vegetable.pumpkin: 1, Lighting.torch: 1}) bone_mill = special_order_recipe(Machine.bone_mill, SpecialOrder.fragments_of_the_past, {Fossil.bone_fragment: 10, Material.clay: 3, Material.stone: 20}) -charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 4, {Material.wood: 20, MetalBar.copper: 2}) +bait_maker = skill_recipe(Machine.bait_maker, Skill.fishing, 6, {MetalBar.iron: 3, WaterItem.coral: 3, WaterItem.sea_urchin: 1}) + +charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 2, {Material.wood: 20, MetalBar.copper: 2}) + crystalarium = skill_recipe(Machine.crystalarium, Skill.mining, 9, {Material.stone: 99, MetalBar.gold: 5, MetalBar.iridium: 2, ArtisanGood.battery_pack: 1}) -furnace = skill_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) +# In-Game, the Furnace recipe is completely unique. It is the only recipe that is obtained in a cutscene after doing a skill-related action. +# So it has a custom source that needs both the craftsanity item from AP and the skill, if craftsanity is enabled. +furnace = skill_craftsanity_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) geode_crusher = special_order_recipe(Machine.geode_crusher, SpecialOrder.cave_patrol, {MetalBar.gold: 2, Material.stone: 50, Mineral.diamond: 1}) +mushroom_log = skill_recipe(Machine.mushroom_log, Skill.foraging, 4, {Material.hardwood: 10, Material.moss: 10}) heavy_tapper = ap_recipe(Machine.heavy_tapper, {Material.hardwood: 30, MetalBar.radioactive: 1}) lightning_rod = skill_recipe(Machine.lightning_rod, Skill.foraging, 6, {MetalBar.iron: 1, MetalBar.quartz: 1, Loot.bat_wing: 5}) ostrich_incubator = ap_recipe(Machine.ostrich_incubator, {Fossil.bone_fragment: 50, Material.hardwood: 50, Currency.cinder_shard: 20}) @@ -234,20 +267,27 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, slime_egg_press = skill_recipe(Machine.slime_egg_press, Skill.combat, 6, {Material.coal: 25, Mineral.fire_quartz: 1, ArtisanGood.battery_pack: 1}) slime_incubator = skill_recipe(Machine.slime_incubator, Skill.combat, 8, {MetalBar.iridium: 2, Loot.slime: 100}) solar_panel = special_order_recipe(Machine.solar_panel, SpecialOrder.island_ingredients, {MetalBar.quartz: 10, MetalBar.iron: 5, MetalBar.gold: 5}) -tapper = skill_recipe(Machine.tapper, Skill.foraging, 3, {Material.wood: 40, MetalBar.copper: 2}) -worm_bin = skill_recipe(Machine.worm_bin, Skill.fishing, 8, {Material.hardwood: 25, MetalBar.gold: 1, MetalBar.iron: 1, Material.fiber: 50}) -tub_o_flowers = festival_shop_recipe(Furniture.tub_o_flowers, Region.flower_dance, 2000, {Material.wood: 15, Seed.tulip: 1, Seed.jazz: 1, Seed.poppy: 1, Seed.spangle: 1}) +tapper = skill_recipe(Machine.tapper, Skill.foraging, 4, {Material.wood: 40, MetalBar.copper: 2}) + +worm_bin = skill_recipe(Machine.worm_bin, Skill.fishing, 4, {Material.hardwood: 25, MetalBar.gold: 1, MetalBar.iron: 1, Material.fiber: 50}) +deluxe_worm_bin = skill_recipe(Machine.deluxe_worm_bin, Skill.fishing, 8, {Machine.worm_bin: 1, Material.moss: 30}) + +tub_o_flowers = festival_shop_recipe(Furniture.tub_o_flowers, LogicRegion.flower_dance, 2000, + {Material.wood: 15, Seed.tulip: 1, Seed.jazz: 1, Seed.poppy: 1, Seed.spangle: 1}) wicked_statue = shop_recipe(Furniture.wicked_statue, Region.sewer, 1000, {Material.stone: 25, Material.coal: 5}) flute_block = cutscene_recipe(Furniture.flute_block, Region.carpenter, NPC.robin, 6, {Material.wood: 10, Ore.copper: 2, Material.fiber: 20}) drum_block = cutscene_recipe(Furniture.drum_block, Region.carpenter, NPC.robin, 6, {Material.stone: 10, Ore.copper: 2, Material.fiber: 20}) chest = starter_recipe(Storage.chest, {Material.wood: 50}) stone_chest = special_order_recipe(Storage.stone_chest, SpecialOrder.robins_resource_rush, {Material.stone: 50}) +big_chest = shop_recipe(Storage.big_chest, Region.carpenter, 5000, {Material.wood: 120, MetalBar.copper: 2}) +big_stone_chest = shop_recipe(Storage.big_stone_chest, LogicRegion.mines_dwarf_shop, 5000, {Material.stone: 250}) wood_sign = starter_recipe(Sign.wood, {Material.wood: 25}) stone_sign = starter_recipe(Sign.stone, {Material.stone: 25}) dark_sign = friendship_recipe(Sign.dark, NPC.krobus, 3, {Loot.bat_wing: 5, Fossil.bone_fragment: 5}) +text_sign = starter_recipe(Sign.text, {Material.wood: 25}) garden_pot = ap_recipe(Craftable.garden_pot, {Material.clay: 1, Material.stone: 10, MetalBar.quartz: 1}, "Greenhouse") scarecrow = skill_recipe(Craftable.scarecrow, Skill.farming, 1, {Material.wood: 50, Material.coal: 1, Material.fiber: 20}) @@ -258,56 +298,84 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, transmute_au = skill_recipe(Craftable.transmute_au, Skill.mining, 7, {MetalBar.iron: 2}) mini_jukebox = cutscene_recipe(Craftable.mini_jukebox, Region.saloon, NPC.gus, 5, {MetalBar.iron: 2, ArtisanGood.battery_pack: 1}) mini_obelisk = special_order_recipe(Craftable.mini_obelisk, SpecialOrder.a_curious_substance, {Material.hardwood: 30, Loot.solar_essence: 20, MetalBar.gold: 3}) -farm_computer = special_order_recipe(Craftable.farm_computer, SpecialOrder.aquatic_overpopulation, {Artifact.dwarf_gadget: 1, ArtisanGood.battery_pack: 1, MetalBar.quartz: 10}) +farm_computer = special_order_recipe(Craftable.farm_computer, SpecialOrder.aquatic_overpopulation, + {Artifact.dwarf_gadget: 1, ArtisanGood.battery_pack: 1, MetalBar.quartz: 10}) hopper = ap_recipe(Craftable.hopper, {Material.hardwood: 10, MetalBar.iridium: 1, MetalBar.radioactive: 1}) -cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 9, {Material.wood: 15, Material.fiber: 10, Material.coal: 3}) + +cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3}) +tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1}) + +statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999}) +statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20}) +heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50}) +mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5}) +treasure_totem = mastery_recipe(Consumable.treasure_totem, Skill.foraging, {Material.hardwood: 5, ArtisanGood.mystic_syrup: 1, Material.moss: 10}) +challenge_bait = mastery_recipe(Fishing.challenge_bait, Skill.fishing, {Fossil.bone_fragment: 5, Material.moss: 2}) +anvil = mastery_recipe(Machine.anvil, Skill.combat, {MetalBar.iron: 50}) +mini_forge = mastery_recipe(Machine.mini_forge, Skill.combat, {Forageable.dragon_tooth: 5, MetalBar.iron: 10, MetalBar.gold: 10, MetalBar.iridium: 5}) travel_charm = shop_recipe(ModCraftable.travel_core, Region.adventurer_guild, 250, {Loot.solar_essence: 1, Loot.void_essence: 1}, ModNames.magic) -preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 2, {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30}, +preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 1, + {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) -preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 7, {MetalBar.copper: 1, Material.hardwood: 15, +restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, ModNames.archaeology) +preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 6, {MetalBar.copper: 1, Material.hardwood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) -grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 8, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1}, ModNames.archaeology) -ancient_battery = skill_recipe(ModMachine.ancient_battery, ModSkill.archaeology, 6, {Material.stone: 40, MetalBar.copper: 10, MetalBar.iron: 5}, +grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 2, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1}, + ModNames.archaeology) +ancient_battery = skill_recipe(ModMachine.ancient_battery, ModSkill.archaeology, 7, {Material.stone: 40, MetalBar.copper: 10, MetalBar.iron: 5}, ModNames.archaeology) -glass_bazier = skill_recipe(ModCraftable.glass_bazier, ModSkill.archaeology, 1, {Artifact.glass_shards: 10}, ModNames.archaeology) -glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 1, {Artifact.glass_shards: 1}, ModNames.archaeology) -glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 1, {Artifact.glass_shards: 5}, ModNames.archaeology) -bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 3, {Fossil.bone_fragment: 1}, ModNames.archaeology) +glass_bazier = skill_recipe(ModCraftable.glass_brazier, ModSkill.archaeology, 4, {Artifact.glass_shards: 10}, ModNames.archaeology) +glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 3, {Artifact.glass_shards: 1}, ModNames.archaeology) +glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 7, {Artifact.glass_shards: 5}, ModNames.archaeology) +bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 4, {Fossil.bone_fragment: 1}, ModNames.archaeology) +rust_path = skill_recipe(ModFloor.rusty_path, ModSkill.archaeology, 2, {ModTrash.rusty_scrap: 2}, ModNames.archaeology) +rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, ModNames.archaeology) +bone_fence = skill_recipe(ModCraftable.bone_fence, ModSkill.archaeology, 8, {Fossil.bone_fragment: 2}, ModNames.archaeology) water_shifter = skill_recipe(ModCraftable.water_shifter, ModSkill.archaeology, 4, {Material.wood: 40, MetalBar.copper: 4}, ModNames.archaeology) -wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 2, {Material.wood: 25}, ModNames.archaeology) +wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 1, {Material.wood: 25}, ModNames.archaeology) hardwood_display = skill_recipe(ModCraftable.hardwood_display, ModSkill.archaeology, 7, {Material.hardwood: 10}, ModNames.archaeology) +lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, ModNames.archaeology) volcano_totem = skill_recipe(ModConsumable.volcano_totem, ModSkill.archaeology, 9, {Material.cinder_shard: 5, Artifact.rare_disc: 1, Artifact.dwarf_gadget: 1}, ModNames.archaeology) -haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, SVEForage.void_soul: 5, Ingredient.sugar: 1, +haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, ModLoot.void_soul: 5, Ingredient.sugar: 1, Meal.spicy_eel: 1}, ModNames.sve) -hero_elixir = shop_recipe(ModEdible.hero_elixir, SVERegion.isaac_shop, 65000, {SVEForage.void_pebble: 3, SVEForage.void_soul: 5, Ingredient.oil: 1, +hero_elixir = shop_recipe(ModEdible.hero_elixir, SVERegion.isaac_shop, 65000, {ModLoot.void_pebble: 3, ModLoot.void_soul: 5, Ingredient.oil: 1, Loot.slime: 10}, ModNames.sve) -armor_elixir = shop_recipe(ModEdible.armor_elixir, SVERegion.alesia_shop, 50000, {Loot.solar_essence: 30, SVEForage.void_soul: 5, Ingredient.vinegar: 5, +armor_elixir = shop_recipe(ModEdible.armor_elixir, SVERegion.alesia_shop, 50000, {Loot.solar_essence: 30, ModLoot.void_soul: 5, Ingredient.vinegar: 5, Fossil.bone_fragment: 5}, ModNames.sve) ginger_tincture = friendship_recipe(ModConsumable.ginger_tincture, ModNPC.goblin, 4, {DistantLandsForageable.brown_amanita: 1, Forageable.ginger: 5, - Material.cinder_shard: 1, DistantLandsForageable.swamp_herb: 1}, ModNames.distant_lands) - -neanderthal_skeleton = shop_recipe(ModCraftable.neanderthal_skeleton, Region.mines_dwarf_shop, 5000, - {ModFossil.neanderthal_skull: 1, ModFossil.neanderthal_ribs: 1, ModFossil.neanderthal_pelvis: 1, ModFossil.neanderthal_limb_bones: 1, - MetalBar.iron: 5, Material.hardwood: 10}, ModNames.boarding_house) -pterodactyl_skeleton_l = shop_recipe(ModCraftable.pterodactyl_skeleton_l, Region.mines_dwarf_shop, 5000, + Material.cinder_shard: 1, DistantLandsForageable.swamp_herb: 1}, + ModNames.distant_lands) + +neanderthal_skeleton = shop_recipe(ModCraftable.neanderthal_skeleton, LogicRegion.mines_dwarf_shop, 5000, + {ModFossil.neanderthal_skull: 1, ModFossil.neanderthal_ribs: 1, ModFossil.neanderthal_pelvis: 1, + ModFossil.neanderthal_limb_bones: 1, + MetalBar.iron: 5, Material.hardwood: 10}, ModNames.boarding_house) +pterodactyl_skeleton_l = shop_recipe(ModCraftable.pterodactyl_skeleton_l, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_skull: 1, ModFossil.pterodactyl_l_wing_bone: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -pterodactyl_skeleton_m = shop_recipe(ModCraftable.pterodactyl_skeleton_m, Region.mines_dwarf_shop, 5000, +pterodactyl_skeleton_m = shop_recipe(ModCraftable.pterodactyl_skeleton_m, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_vertebra: 1, ModFossil.pterodactyl_ribs: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -pterodactyl_skeleton_r = shop_recipe(ModCraftable.pterodactyl_skeleton_r, Region.mines_dwarf_shop, 5000, +pterodactyl_skeleton_r = shop_recipe(ModCraftable.pterodactyl_skeleton_r, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_claw: 1, ModFossil.pterodactyl_r_wing_bone: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_l = shop_recipe(ModCraftable.trex_skeleton_l, Region.mines_dwarf_shop, 5000, +trex_skeleton_l = shop_recipe(ModCraftable.trex_skeleton_l, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_tooth: 1, ModFossil.dinosaur_skull: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_m = shop_recipe(ModCraftable.trex_skeleton_m, Region.mines_dwarf_shop, 5000, +trex_skeleton_m = shop_recipe(ModCraftable.trex_skeleton_m, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_ribs: 1, ModFossil.dinosaur_claw: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_r = shop_recipe(ModCraftable.trex_skeleton_r, Region.mines_dwarf_shop, 5000, +trex_skeleton_r = shop_recipe(ModCraftable.trex_skeleton_r, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_femur: 1, ModFossil.dinosaur_pelvis: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) +bouquet = skill_recipe(Gift.bouquet, ModSkill.socializing, 3, {Flower.tulip: 3}, ModNames.socializing_skill) +trash_bin = skill_recipe(ModMachine.trash_bin, ModSkill.binning, 2, {Material.stone: 30, MetalBar.iron: 2}, ModNames.binning_skill) +composter = skill_recipe(ModMachine.composter, ModSkill.binning, 4, {Material.wood: 70, Material.sap: 20, Material.fiber: 30}, ModNames.binning_skill) +recycling_bin = skill_recipe(ModMachine.recycling_bin, ModSkill.binning, 7, {MetalBar.iron: 3, Material.fiber: 10, MetalBar.gold: 2}, ModNames.binning_skill) +advanced_recycling_machine = skill_recipe(ModMachine.advanced_recycling_machine, ModSkill.binning, 9, + {MetalBar.iridium: 5, ArtisanGood.battery_pack: 2, MetalBar.quartz: 10}, ModNames.binning_skill) + all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes} diff --git a/worlds/stardew_valley/data/crops.csv b/worlds/stardew_valley/data/crops.csv deleted file mode 100644 index 0bf43a76764e..000000000000 --- a/worlds/stardew_valley/data/crops.csv +++ /dev/null @@ -1,41 +0,0 @@ -crop,farm_growth_seasons,seed,seed_seasons,seed_regions,requires_island -Amaranth,Fall,Amaranth Seeds,Fall,"Pierre's General Store",False -Artichoke,Fall,Artichoke Seeds,Fall,"Pierre's General Store",False -Beet,Fall,Beet Seeds,Fall,Oasis,False -Blue Jazz,Spring,Jazz Seeds,Spring,"Pierre's General Store",False -Blueberry,Summer,Blueberry Seeds,Summer,"Pierre's General Store",False -Bok Choy,Fall,Bok Choy Seeds,Fall,"Pierre's General Store",False -Cactus Fruit,,Cactus Seeds,,Oasis,False -Cauliflower,Spring,Cauliflower Seeds,Spring,"Pierre's General Store",False -Coffee Bean,"Spring,Summer",Coffee Bean,"Summer,Fall","Traveling Cart",False -Corn,"Summer,Fall",Corn Seeds,"Summer,Fall","Pierre's General Store",False -Cranberries,Fall,Cranberry Seeds,Fall,"Pierre's General Store",False -Eggplant,Fall,Eggplant Seeds,Fall,"Pierre's General Store",False -Fairy Rose,Fall,Fairy Seeds,Fall,"Pierre's General Store",False -Garlic,Spring,Garlic Seeds,Spring,"Pierre's General Store",False -Grape,Fall,Grape Starter,Fall,"Pierre's General Store",False -Green Bean,Spring,Bean Starter,Spring,"Pierre's General Store",False -Hops,Summer,Hops Starter,Summer,"Pierre's General Store",False -Hot Pepper,Summer,Pepper Seeds,Summer,"Pierre's General Store",False -Kale,Spring,Kale Seeds,Spring,"Pierre's General Store",False -Melon,Summer,Melon Seeds,Summer,"Pierre's General Store",False -Parsnip,Spring,Parsnip Seeds,Spring,"Pierre's General Store",False -Pineapple,Summer,Pineapple Seeds,Summer,"Island Trader",True -Poppy,Summer,Poppy Seeds,Summer,"Pierre's General Store",False -Potato,Spring,Potato Seeds,Spring,"Pierre's General Store",False -Qi Fruit,"Spring,Summer,Fall,Winter",Qi Bean,"Spring,Summer,Fall,Winter","Qi's Walnut Room",True -Pumpkin,Fall,Pumpkin Seeds,Fall,"Pierre's General Store",False -Radish,Summer,Radish Seeds,Summer,"Pierre's General Store",False -Red Cabbage,Summer,Red Cabbage Seeds,Summer,"Pierre's General Store",False -Rhubarb,Spring,Rhubarb Seeds,Spring,Oasis,False -Starfruit,Summer,Starfruit Seeds,Summer,Oasis,False -Strawberry,Spring,Strawberry Seeds,Spring,"Pierre's General Store",False -Summer Spangle,Summer,Spangle Seeds,Summer,"Pierre's General Store",False -Sunflower,"Summer,Fall",Sunflower Seeds,"Summer,Fall","Pierre's General Store",False -Sweet Gem Berry,Fall,Rare Seed,"Spring,Summer",Traveling Cart,False -Taro Root,Summer,Taro Tuber,Summer,"Island Trader",True -Tomato,Summer,Tomato Seeds,Summer,"Pierre's General Store",False -Tulip,Spring,Tulip Bulb,Spring,"Pierre's General Store",False -Unmilled Rice,Spring,Rice Shoot,Spring,"Pierre's General Store",False -Wheat,"Summer,Fall",Wheat Seeds,"Summer,Fall","Pierre's General Store",False -Yam,Fall,Yam Seeds,Fall,"Pierre's General Store",False diff --git a/worlds/stardew_valley/data/crops_data.py b/worlds/stardew_valley/data/crops_data.py deleted file mode 100644 index 7144ccfbcf9b..000000000000 --- a/worlds/stardew_valley/data/crops_data.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from typing import Tuple - -from .. import data - - -@dataclass(frozen=True) -class SeedItem: - name: str - seasons: Tuple[str] - regions: Tuple[str] - requires_island: bool - - -@dataclass(frozen=True) -class CropItem: - name: str - farm_growth_seasons: Tuple[str] - seed: SeedItem - - -def load_crop_csv(): - import csv - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files # noqa - - with files(data).joinpath("crops.csv").open() as file: - reader = csv.DictReader(file) - crops = [] - seeds = [] - - for item in reader: - seeds.append(SeedItem(item["seed"], - tuple(season for season in item["seed_seasons"].split(",")) - if item["seed_seasons"] else tuple(), - tuple(region for region in item["seed_regions"].split(",")) - if item["seed_regions"] else tuple(), - item["requires_island"] == "True")) - crops.append(CropItem(item["crop"], - tuple(season for season in item["farm_growth_seasons"].split(",")) - if item["farm_growth_seasons"] else tuple(), - seeds[-1])) - return crops, seeds - - -# TODO Those two should probably be split to we can include rest of seeds -all_crops, all_purchasable_seeds = load_crop_csv() -crops_by_name = {crop.name: crop for crop in all_crops} diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index aeb416733950..dfa8891077ee 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import List, Tuple, Union, Optional, Set +from typing import Tuple, Union, Optional from . import season_data as season -from ..strings.fish_names import Fish, SVEFish, DistantLandsFish -from ..strings.region_names import Region, SVERegion from ..mods.mod_data import ModNames +from ..strings.fish_names import Fish, SVEFish, DistantLandsFish +from ..strings.region_names import Region, SVERegion, LogicRegion @dataclass(frozen=True) @@ -26,10 +26,12 @@ 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,) forest_river = (Region.forest,) +forest_waterfall = (LogicRegion.forest_waterfall,) secret_woods = (Region.secret_woods,) mines_floor_20 = (Region.mines_floor_20,) mines_floor_60 = (Region.mines_floor_60,) @@ -45,13 +47,12 @@ def __repr__(self): crimson_badlands = (SVERegion.crimson_badlands,) shearwater = (SVERegion.shearwater,) -highlands = (SVERegion.highlands_outside,) +highlands_pond = (SVERegion.highlands_pond,) +highlands_cave = (SVERegion.highlands_cavern,) sprite_spring = (SVERegion.sprite_spring,) fable_reef = (SVERegion.fable_reef,) vineyard = (SVERegion.blue_moon_vineyard,) -all_fish: List[FishItem] = [] - def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple[str, ...]], difficulty: int, legendary: bool = False, extended_family: bool = False, mod_name: Optional[str] = None) -> FishItem: @@ -59,72 +60,72 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple seasons = (seasons,) fish_item = FishItem(name, locations, seasons, difficulty, legendary, extended_family, mod_name) - all_fish.append(fish_item) return fish_item -albacore = create_fish("Albacore", ocean, (season.fall, season.winter), 60) -anchovy = create_fish("Anchovy", ocean, (season.spring, season.fall), 30) -blue_discus = create_fish("Blue Discus", ginger_island_river, season.all_seasons, 60) -bream = create_fish("Bream", town_river + forest_river, season.all_seasons, 35) -bullhead = create_fish("Bullhead", mountain_lake, season.all_seasons, 46) +albacore = create_fish(Fish.albacore, ocean, (season.fall, season.winter), 60) +anchovy = create_fish(Fish.anchovy, ocean, (season.spring, season.fall), 30) +blue_discus = create_fish(Fish.blue_discus, ginger_island_river, season.all_seasons, 60) +bream = create_fish(Fish.bream, town_river + forest_river, season.all_seasons, 35) +bullhead = create_fish(Fish.bullhead, mountain_lake, season.all_seasons, 46) carp = create_fish(Fish.carp, mountain_lake + secret_woods + sewers + mutant_bug_lair, season.not_winter, 15) -catfish = create_fish("Catfish", town_river + forest_river + secret_woods, (season.spring, season.fall), 75) -chub = create_fish("Chub", forest_river + mountain_lake, season.all_seasons, 35) -dorado = create_fish("Dorado", forest_river, season.summer, 78) -eel = create_fish("Eel", ocean, (season.spring, season.fall), 70) -flounder = create_fish("Flounder", ocean, (season.spring, season.summer), 50) -ghostfish = create_fish("Ghostfish", mines_floor_20 + mines_floor_60, season.all_seasons, 50) -halibut = create_fish("Halibut", ocean, season.not_fall, 50) -herring = create_fish("Herring", ocean, (season.spring, season.winter), 25) -ice_pip = create_fish("Ice Pip", mines_floor_60, season.all_seasons, 85) -largemouth_bass = create_fish("Largemouth Bass", mountain_lake, season.all_seasons, 50) -lava_eel = create_fish("Lava Eel", mines_floor_100, season.all_seasons, 90) -lingcod = create_fish("Lingcod", town_river + forest_river + mountain_lake, season.winter, 85) -lionfish = create_fish("Lionfish", ginger_island_ocean, season.all_seasons, 50) -midnight_carp = create_fish("Midnight Carp", mountain_lake + forest_pond + ginger_island_river, +catfish = create_fish(Fish.catfish, town_river + forest_river + secret_woods, (season.spring, season.fall), 75) +chub = create_fish(Fish.chub, forest_river + mountain_lake, season.all_seasons, 35) +dorado = create_fish(Fish.dorado, forest_river, season.summer, 78) +eel = create_fish(Fish.eel, ocean, (season.spring, season.fall), 70) +flounder = create_fish(Fish.flounder, ocean, (season.spring, season.summer), 50) +ghostfish = create_fish(Fish.ghostfish, mines_floor_20 + mines_floor_60, season.all_seasons, 50) +goby = create_fish(Fish.goby, forest_waterfall, season.all_seasons, 55) +halibut = create_fish(Fish.halibut, ocean, season.not_fall, 50) +herring = create_fish(Fish.herring, ocean, (season.spring, season.winter), 25) +ice_pip = create_fish(Fish.ice_pip, mines_floor_60, season.all_seasons, 85) +largemouth_bass = create_fish(Fish.largemouth_bass, mountain_lake, season.all_seasons, 50) +lava_eel = create_fish(Fish.lava_eel, mines_floor_100, season.all_seasons, 90) +lingcod = create_fish(Fish.lingcod, town_river + forest_river + mountain_lake, season.winter, 85) +lionfish = create_fish(Fish.lionfish, ginger_island_ocean, season.all_seasons, 50) +midnight_carp = create_fish(Fish.midnight_carp, mountain_lake + forest_pond + ginger_island_river, (season.fall, season.winter), 55) -octopus = create_fish("Octopus", ocean, season.summer, 95) -perch = create_fish("Perch", town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) -pike = create_fish("Pike", town_river + forest_river + forest_pond, (season.summer, season.winter), 60) -pufferfish = create_fish("Pufferfish", ocean + ginger_island_ocean, season.summer, 80) -rainbow_trout = create_fish("Rainbow Trout", town_river + forest_river + mountain_lake, season.summer, 45) -red_mullet = create_fish("Red Mullet", ocean, (season.summer, season.winter), 55) -red_snapper = create_fish("Red Snapper", ocean, (season.summer, season.fall), 40) -salmon = create_fish("Salmon", town_river + forest_river, season.fall, 50) -sandfish = create_fish("Sandfish", desert, season.all_seasons, 65) -sardine = create_fish("Sardine", ocean, (season.spring, season.fall, season.winter), 30) -scorpion_carp = create_fish("Scorpion Carp", desert, season.all_seasons, 90) -sea_cucumber = create_fish("Sea Cucumber", ocean, (season.fall, season.winter), 40) -shad = create_fish("Shad", town_river + forest_river, season.not_winter, 45) -slimejack = create_fish("Slimejack", mutant_bug_lair, season.all_seasons, 55) -smallmouth_bass = create_fish("Smallmouth Bass", town_river + forest_river, (season.spring, season.fall), 28) -squid = create_fish("Squid", ocean, season.winter, 75) -stingray = create_fish("Stingray", pirate_cove, season.all_seasons, 80) -stonefish = create_fish("Stonefish", mines_floor_20, season.all_seasons, 65) -sturgeon = create_fish("Sturgeon", mountain_lake, (season.summer, season.winter), 78) -sunfish = create_fish("Sunfish", town_river + forest_river, (season.spring, season.summer), 30) -super_cucumber = create_fish("Super Cucumber", ocean + ginger_island_ocean, (season.summer, season.fall), 80) -tiger_trout = create_fish("Tiger Trout", town_river + forest_river, (season.fall, season.winter), 60) -tilapia = create_fish("Tilapia", ocean + ginger_island_ocean, (season.summer, season.fall), 50) +octopus = create_fish(Fish.octopus, ocean, season.summer, 95) +perch = create_fish(Fish.perch, town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) +pike = create_fish(Fish.pike, town_river + forest_river + forest_pond, (season.summer, season.winter), 60) +pufferfish = create_fish(Fish.pufferfish, ocean + ginger_island_ocean, season.summer, 80) +rainbow_trout = create_fish(Fish.rainbow_trout, town_river + forest_river + mountain_lake, season.summer, 45) +red_mullet = create_fish(Fish.red_mullet, ocean, (season.summer, season.winter), 55) +red_snapper = create_fish(Fish.red_snapper, ocean, (season.summer, season.fall), 40) +salmon = create_fish(Fish.salmon, town_river + forest_river, season.fall, 50) +sandfish = create_fish(Fish.sandfish, desert, season.all_seasons, 65) +sardine = create_fish(Fish.sardine, ocean, (season.spring, season.fall, season.winter), 30) +scorpion_carp = create_fish(Fish.scorpion_carp, desert, season.all_seasons, 90) +sea_cucumber = create_fish(Fish.sea_cucumber, ocean, (season.fall, season.winter), 40) +shad = create_fish(Fish.shad, town_river + forest_river, season.not_winter, 45) +slimejack = create_fish(Fish.slimejack, mutant_bug_lair, season.all_seasons, 55) +smallmouth_bass = create_fish(Fish.smallmouth_bass, town_river + forest_river, (season.spring, season.fall), 28) +squid = create_fish(Fish.squid, ocean, season.winter, 75) +stingray = create_fish(Fish.stingray, pirate_cove, season.all_seasons, 80) +stonefish = create_fish(Fish.stonefish, mines_floor_20, season.all_seasons, 65) +sturgeon = create_fish(Fish.sturgeon, mountain_lake, (season.summer, season.winter), 78) +sunfish = create_fish(Fish.sunfish, town_river + forest_river, (season.spring, season.summer), 30) +super_cucumber = create_fish(Fish.super_cucumber, ocean + ginger_island_ocean, (season.summer, season.fall), 80) +tiger_trout = create_fish(Fish.tiger_trout, town_river + forest_river, (season.fall, season.winter), 60) +tilapia = create_fish(Fish.tilapia, ocean + ginger_island_ocean, (season.summer, season.fall), 50) # Tuna has different seasons on ginger island. Should be changed when the whole fish thing is refactored -tuna = create_fish("Tuna", ocean + ginger_island_ocean, (season.summer, season.winter), 70) -void_salmon = create_fish("Void Salmon", witch_swamp, season.all_seasons, 80) -walleye = create_fish("Walleye", town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) -woodskip = create_fish("Woodskip", secret_woods, season.all_seasons, 50) +tuna = create_fish(Fish.tuna, ocean + ginger_island_ocean, (season.summer, season.winter), 70) +void_salmon = create_fish(Fish.void_salmon, witch_swamp, season.all_seasons, 80) +walleye = create_fish(Fish.walleye, town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) +woodskip = create_fish(Fish.woodskip, secret_woods, season.all_seasons, 50) -blob_fish = create_fish("Blobfish", night_market, season.winter, 75) -midnight_squid = create_fish("Midnight Squid", night_market, season.winter, 55) -spook_fish = create_fish("Spook Fish", night_market, season.winter, 60) +blobfish = create_fish(Fish.blobfish, night_market, season.winter, 75) +midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 55) +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) @@ -134,9 +135,9 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple bull_trout = create_fish(SVEFish.bull_trout, forest_river, season.not_spring, 45, mod_name=ModNames.sve) butterfish = create_fish(SVEFish.butterfish, shearwater, season.not_winter, 75, mod_name=ModNames.sve) clownfish = create_fish(SVEFish.clownfish, ginger_island_ocean, season.all_seasons, 45, mod_name=ModNames.sve) -daggerfish = create_fish(SVEFish.daggerfish, highlands, season.all_seasons, 50, mod_name=ModNames.sve) +daggerfish = create_fish(SVEFish.daggerfish, highlands_pond, season.all_seasons, 50, mod_name=ModNames.sve) frog = create_fish(SVEFish.frog, mountain_lake, (season.spring, season.summer), 70, mod_name=ModNames.sve) -gemfish = create_fish(SVEFish.gemfish, highlands, season.all_seasons, 100, mod_name=ModNames.sve) +gemfish = create_fish(SVEFish.gemfish, highlands_cave, season.all_seasons, 100, mod_name=ModNames.sve) goldenfish = create_fish(SVEFish.goldenfish, sprite_spring, season.all_seasons, 60, mod_name=ModNames.sve) grass_carp = create_fish(SVEFish.grass_carp, secret_woods, (season.spring, season.summer), 85, mod_name=ModNames.sve) king_salmon = create_fish(SVEFish.king_salmon, forest_river, (season.spring, season.summer), 80, mod_name=ModNames.sve) @@ -155,37 +156,21 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple void_eel = create_fish(SVEFish.void_eel, witch_swamp, season.all_seasons, 100, mod_name=ModNames.sve) water_grub = create_fish(SVEFish.water_grub, mutant_bug_lair, season.all_seasons, 60, mod_name=ModNames.sve) sea_sponge = create_fish(SVEFish.sea_sponge, ginger_island_ocean, season.all_seasons, 40, mod_name=ModNames.sve) -dulse_seaweed = create_fish(SVEFish.dulse_seaweed, vineyard, season.all_seasons, 50, mod_name=ModNames.sve) void_minnow = create_fish(DistantLandsFish.void_minnow, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) purple_algae = create_fish(DistantLandsFish.purple_algae, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) swamp_leech = create_fish(DistantLandsFish.swamp_leech, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) giant_horsehoe_crab = create_fish(DistantLandsFish.giant_horsehoe_crab, witch_swamp, season.all_seasons, 90, mod_name=ModNames.distant_lands) - -clam = create_fish("Clam", ocean, season.all_seasons, -1) -cockle = create_fish("Cockle", ocean, season.all_seasons, -1) -crab = create_fish("Crab", ocean, season.all_seasons, -1) -crayfish = create_fish("Crayfish", fresh_water, season.all_seasons, -1) -lobster = create_fish("Lobster", ocean, season.all_seasons, -1) -mussel = create_fish("Mussel", ocean, season.all_seasons, -1) -oyster = create_fish("Oyster", ocean, season.all_seasons, -1) -periwinkle = create_fish("Periwinkle", fresh_water, season.all_seasons, -1) -shrimp = create_fish("Shrimp", ocean, season.all_seasons, -1) -snail = create_fish("Snail", fresh_water, season.all_seasons, -1) - -legendary_fish = [angler, crimsonfish, glacierfish, legend, mutant_carp] -extended_family = [ms_angler, son_of_crimsonfish, glacierfish_jr, legend_ii, radioactive_carp] -special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] -island_fish = [lionfish, blue_discus, stingray, *extended_family] - -all_fish_by_name = {fish.name: fish for fish in all_fish} - - -def get_fish_for_mods(mods: Set[str]) -> List[FishItem]: - fish_for_mods = [] - for fish in all_fish: - if fish.mod_name and fish.mod_name not in mods: - continue - fish_for_mods.append(fish) - return fish_for_mods +clam = create_fish(Fish.clam, ocean, season.all_seasons, -1) +cockle = create_fish(Fish.cockle, ocean, season.all_seasons, -1) +crab = create_fish(Fish.crab, ocean, season.all_seasons, -1) +crayfish = create_fish(Fish.crayfish, fresh_water, season.all_seasons, -1) +lobster = create_fish(Fish.lobster, ocean, season.all_seasons, -1) +mussel = create_fish(Fish.mussel, ocean, season.all_seasons, -1) +oyster = create_fish(Fish.oyster, ocean, season.all_seasons, -1) +periwinkle = create_fish(Fish.periwinkle, fresh_water, season.all_seasons, -1) +shrimp = create_fish(Fish.shrimp, ocean, season.all_seasons, -1) +snail = create_fish(Fish.snail, fresh_water, season.all_seasons, -1) + +vanilla_legendary_fish = [angler, crimsonfish, glacierfish, legend, mutant_carp] diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py new file mode 100644 index 000000000000..c6e4717cd1e0 --- /dev/null +++ b/worlds/stardew_valley/data/game_item.py @@ -0,0 +1,81 @@ +import enum +from abc import ABC +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import List, Iterable, Set, ClassVar, Tuple, Mapping, Callable, Any + +from ..stardew_rule.protocol import StardewRule + +DEFAULT_REQUIREMENT_TAGS = MappingProxyType({}) + + +@dataclass(frozen=True) +class Requirement(ABC): + ... + + +class ItemTag(enum.Enum): + CROPSANITY_SEED = enum.auto() + CROPSANITY = enum.auto() + FISH = enum.auto() + FRUIT = enum.auto() + VEGETABLE = enum.auto() + EDIBLE_MUSHROOM = enum.auto() + BOOK = enum.auto() + BOOK_POWER = enum.auto() + BOOK_SKILL = enum.auto() + + +@dataclass(frozen=True) +class ItemSource(ABC): + add_tags: ClassVar[Tuple[ItemTag]] = () + + other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple) + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return DEFAULT_REQUIREMENT_TAGS + + +@dataclass(frozen=True, kw_only=True) +class GenericSource(ItemSource): + regions: Tuple[str, ...] = () + """No region means it's available everywhere.""" + + +@dataclass(frozen=True) +class CustomRuleSource(ItemSource): + """Hopefully once everything is migrated to sources, we won't need these custom logic anymore.""" + create_rule: Callable[[Any], StardewRule] + + +@dataclass(frozen=True, kw_only=True) +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, ...] + + def __init__(self, *tag: ItemTag): + self.tag = tag # noqa + + @property + def add_tags(self): + return self.tag + + +@dataclass(frozen=True) +class GameItem: + name: str + sources: List[ItemSource] = field(default_factory=list) + tags: Set[ItemTag] = field(default_factory=set) + + def add_sources(self, sources: Iterable[ItemSource]): + self.sources.extend(source for source in sources if type(source) is not Tag) + for source in sources: + self.add_tags(source.add_tags) + + def add_tags(self, tags: Iterable[ItemTag]): + self.tags.update(tags) diff --git a/worlds/stardew_valley/data/harvest.py b/worlds/stardew_valley/data/harvest.py new file mode 100644 index 000000000000..0fdae9549587 --- /dev/null +++ b/worlds/stardew_valley/data/harvest.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass +from typing import Tuple, Sequence, Mapping + +from .game_item import ItemSource, ItemTag +from ..strings.season_names import Season + + +@dataclass(frozen=True, kw_only=True) +class ForagingSource(ItemSource): + regions: Tuple[str, ...] + seasons: Tuple[str, ...] = Season.all + + +@dataclass(frozen=True, kw_only=True) +class SeasonalForagingSource(ItemSource): + season: str + days: Sequence[int] + regions: Tuple[str, ...] + + def as_foraging_source(self) -> ForagingSource: + return ForagingSource(seasons=(self.season,), regions=self.regions) + + +@dataclass(frozen=True, kw_only=True) +class FruitBatsSource(ItemSource): + ... + + +@dataclass(frozen=True, kw_only=True) +class MushroomCaveSource(ItemSource): + ... + + +@dataclass(frozen=True, kw_only=True) +class HarvestFruitTreeSource(ItemSource): + add_tags = (ItemTag.CROPSANITY,) + + sapling: str + seasons: Tuple[str, ...] = Season.all + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return { + self.sapling: (ItemTag.CROPSANITY_SEED,) + } + + +@dataclass(frozen=True, kw_only=True) +class HarvestCropSource(ItemSource): + add_tags = (ItemTag.CROPSANITY,) + + seed: str + seasons: Tuple[str, ...] = Season.all + """Empty means it can't be grown on the farm.""" + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return { + self.seed: (ItemTag.CROPSANITY_SEED,) + } + + +@dataclass(frozen=True, kw_only=True) +class ArtifactSpotSource(ItemSource): + amount: int diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 9ecb2ba3649e..05af275ba472 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -7,7 +7,7 @@ id,name,classification,groups,mod_name 19,Glittering Boulder Removed,progression,COMMUNITY_REWARD, 20,Minecarts Repair,useful,COMMUNITY_REWARD, 21,Bus Repair,progression,COMMUNITY_REWARD, -22,Progressive Movie Theater,progression,COMMUNITY_REWARD, +22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD, 23,Stardrop,progression,, 24,Progressive Backpack,progression,, 25,Rusty Sword,filler,"WEAPON,DEPRECATED", @@ -54,7 +54,7 @@ id,name,classification,groups,mod_name 68,Progressive Watering Can,progression,PROGRESSIVE_TOOLS, 69,Progressive Trash Can,progression,PROGRESSIVE_TOOLS, 70,Progressive Fishing Rod,progression,PROGRESSIVE_TOOLS, -71,Golden Scythe,useful,, +71,Golden Scythe,useful,DEPRECATED, 72,Progressive Mine Elevator,progression,, 73,Farming Level,progression,SKILL_LEVEL_UP, 74,Fishing Level,progression,SKILL_LEVEL_UP, @@ -92,8 +92,8 @@ id,name,classification,groups,mod_name 106,Galaxy Sword,filler,"WEAPON,DEPRECATED", 107,Galaxy Dagger,filler,"WEAPON,DEPRECATED", 108,Galaxy Hammer,filler,"WEAPON,DEPRECATED", -109,Movement Speed Bonus,progression,, -110,Luck Bonus,progression,, +109,Movement Speed Bonus,useful,, +110,Luck Bonus,filler,PLAYER_BUFF, 111,Lava Katana,filler,"WEAPON,DEPRECATED", 112,Progressive House,progression,, 113,Traveling Merchant: Sunday,progression,TRAVELING_MERCHANT_DAY, @@ -104,7 +104,7 @@ id,name,classification,groups,mod_name 118,Traveling Merchant: Friday,progression,TRAVELING_MERCHANT_DAY, 119,Traveling Merchant: Saturday,progression,TRAVELING_MERCHANT_DAY, 120,Traveling Merchant Stock Size,useful,, -121,Traveling Merchant Discount,useful,, +121,Traveling Merchant Discount,useful,DEPRECATED, 122,Return Scepter,useful,, 123,Progressive Season,progression,, 124,Spring,progression,SEASON, @@ -307,7 +307,7 @@ id,name,classification,groups,mod_name 322,Phoenix Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 323,Immunity Band,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 324,Glowstone Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", -325,Fairy Dust Recipe,progression,, +325,Fairy Dust Recipe,progression,"GINGER_ISLAND", 326,Heavy Tapper Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 327,Hyper Speed-Gro Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 328,Deluxe Fertilizer Recipe,progression,QI_CRAFTING_RECIPE, @@ -398,6 +398,7 @@ id,name,classification,groups,mod_name 417,Tropical Curry Recipe,progression,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE", 418,Trout Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS", 419,Vegetable Medley Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", +420,Moss Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL", 425,Gate Recipe,progression,CRAFTSANITY, 426,Wood Fence Recipe,progression,CRAFTSANITY, 427,Deluxe Retaining Soil Recipe,progression,"CRAFTSANITY,GINGER_ISLAND", @@ -430,7 +431,7 @@ id,name,classification,groups,mod_name 454,Marble Brazier Recipe,progression,CRAFTSANITY, 455,Wood Lamp-post Recipe,progression,CRAFTSANITY, 456,Iron Lamp-post Recipe,progression,CRAFTSANITY, -457,Furnace Recipe,progression,CRAFTSANITY, +457,Furnace Recipe,progression,"CRAFTSANITY", 458,Wicked Statue Recipe,progression,CRAFTSANITY, 459,Chest Recipe,progression,CRAFTSANITY, 460,Wood Sign Recipe,progression,CRAFTSANITY, @@ -439,6 +440,75 @@ id,name,classification,groups,mod_name 470,Fruit Bats,progression,, 471,Mushroom Boxes,progression,, 475,The Gateway Gazette,progression,TV_CHANNEL, +476,Carrot Seeds,progression,CROPSANITY, +477,Summer Squash Seeds,progression,CROPSANITY, +478,Broccoli Seeds,progression,CROPSANITY, +479,Powdermelon Seeds,progression,CROPSANITY, +480,Progressive Raccoon,progression,, +481,Farming Mastery,progression,SKILL_MASTERY, +482,Mining Mastery,progression,SKILL_MASTERY, +483,Foraging Mastery,progression,SKILL_MASTERY, +484,Fishing Mastery,progression,SKILL_MASTERY, +485,Combat Mastery,progression,SKILL_MASTERY, +486,Fish Smoker Recipe,progression,CRAFTSANITY, +487,Dehydrator Recipe,progression,CRAFTSANITY, +488,Big Chest Recipe,progression,CRAFTSANITY, +489,Big Stone Chest Recipe,progression,CRAFTSANITY, +490,Text Sign Recipe,progression,CRAFTSANITY, +491,Blue Grass Starter Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", +492,Mastery Of The Five Ways,progression,SKILL_MASTERY, +493,Progressive Scythe,useful,, +494,Progressive Pan,progression,PROGRESSIVE_TOOLS, +495,Calico Statue,filler,FESTIVAL, +496,Mummy Mask,filler,FESTIVAL, +497,Free Cactis,filler,FESTIVAL, +498,Gil's Hat,filler,FESTIVAL, +499,Bucket Hat,filler,FESTIVAL, +500,Mounted Trout,filler,FESTIVAL, +501,'Squid Kid',filler,FESTIVAL, +502,Squid Hat,filler,FESTIVAL, +503,Resource Pack: 200 Calico Egg,useful,"FESTIVAL", +504,Resource Pack: 120 Calico Egg,useful,"FESTIVAL", +505,Resource Pack: 100 Calico Egg,useful,"FESTIVAL", +506,Resource Pack: 50 Calico Egg,useful,"FESTIVAL", +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,progression,"FESTIVAL", +511,Mr Qi's Plane Ride,progression,, +521,Power: Price Catalogue,useful,"BOOK_POWER", +522,Power: Mapping Cave Systems,useful,"BOOK_POWER", +523,Power: Way Of The Wind pt. 1,progression,"BOOK_POWER", +524,Power: Way Of The Wind pt. 2,useful,"BOOK_POWER", +525,Power: Monster Compendium,useful,"BOOK_POWER", +526,Power: Friendship 101,useful,"BOOK_POWER", +527,"Power: Jack Be Nimble, Jack Be Thick",useful,"BOOK_POWER", +528,Power: Woody's Secret,useful,"BOOK_POWER", +529,Power: Raccoon Journal,useful,"BOOK_POWER", +530,Power: Jewels Of The Sea,useful,"BOOK_POWER", +531,Power: Dwarvish Safety Manual,useful,"BOOK_POWER", +532,Power: The Art O' Crabbing,useful,"BOOK_POWER", +533,Power: The Alleyway Buffet,useful,"BOOK_POWER", +534,Power: The Diamond Hunter,useful,"BOOK_POWER", +535,Power: Book of Mysteries,progression,"BOOK_POWER", +536,Power: Horse: The Book,useful,"BOOK_POWER", +537,Power: Treasure Appraisal Guide,useful,"BOOK_POWER", +538,Power: Ol' Slitherlegs,useful,"BOOK_POWER", +539,Power: Animal Catalogue,useful,"BOOK_POWER", +541,Progressive Lost Book,progression,"LOST_BOOK", +551,Golden Walnut,progression,"RESOURCE_PACK,GINGER_ISLAND", +552,3 Golden Walnuts,progression,"GINGER_ISLAND", +553,5 Golden Walnuts,progression,"GINGER_ISLAND", +554,Damage Bonus,filler,PLAYER_BUFF, +555,Defense Bonus,filler,PLAYER_BUFF, +556,Immunity Bonus,filler,PLAYER_BUFF, +557,Health Bonus,filler,PLAYER_BUFF, +558,Energy Bonus,filler,PLAYER_BUFF, +559,Bite Rate Bonus,filler,PLAYER_BUFF, +560,Fish Trap Bonus,filler,PLAYER_BUFF, +561,Fishing Bar Size Bonus,filler,PLAYER_BUFF, +562,Quality Bonus,filler,PLAYER_BUFF, +563,Glow Bonus,filler,PLAYER_BUFF, 4001,Burnt Trap,trap,TRAP, 4002,Darkness Trap,trap,TRAP, 4003,Frozen Trap,trap,TRAP, @@ -464,6 +534,8 @@ id,name,classification,groups,mod_name 4023,Benjamin Budton Trap,trap,TRAP, 4024,Inflation Trap,trap,TRAP, 4025,Bomb Trap,trap,TRAP, +4026,Nudge Trap,trap,TRAP, +4501,Deflation Bonus,filler,, 5000,Resource Pack: 500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", 5001,Resource Pack: 1000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", 5002,Resource Pack: 1500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", @@ -701,9 +773,9 @@ id,name,classification,groups,mod_name 5234,Resource Pack: 10 Qi Seasoning,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5235,Mr. Qi's Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK", 5236,Aquatic Sanctuary,filler,RESOURCE_PACK, +5237,Leprechaun Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK", 5242,Exotic Double Bed,filler,RESOURCE_PACK, 5243,Resource Pack: 2 Qi Gem,filler,"GINGER_ISLAND,RESOURCE_PACK,DEPRECATED", -5245,Golden Walnut,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL,GINGER_ISLAND", 5247,Fairy Dust,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5248,Seed Maker,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5249,Keg,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", @@ -726,6 +798,28 @@ id,name,classification,groups,mod_name 5266,Resource Pack: 5 Staircase,filler,"RESOURCE_PACK", 5267,Auto-Petter,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5268,Auto-Grabber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5269,Resource Pack: 10 Calico Egg,filler,"RESOURCE_PACK", +5270,Resource Pack: 20 Calico Egg,filler,"RESOURCE_PACK", +5272,Tent Kit,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5273,Resource Pack: 4 Mystery Box,filler,"RESOURCE_PACK", +5274,Prize Ticket,filler,"RESOURCE_PACK", +5275,Resource Pack: 20 Deluxe Bait,filler,"RESOURCE_PACK", +5276,Resource Pack: 2 Triple Shot Espresso,filler,"RESOURCE_PACK", +5277,Dish O' The Sea,filler,"RESOURCE_PACK", +5278,Seafoam Pudding,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5279,Trap Bobber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5280,Treasure Chest,filler,"RESOURCE_PACK", +5281,Resource Pack: 15 Mixed Seeds,filler,"RESOURCE_PACK", +5282,Resource Pack: 15 Mixed Flower Seeds,filler,"RESOURCE_PACK", +5283,Resource Pack: 5 Cherry Bomb,filler,"RESOURCE_PACK", +5284,Resource Pack: 3 Bomb,filler,"RESOURCE_PACK", +5285,Resource Pack: 2 Mega Bomb,filler,"RESOURCE_PACK", +5286,Resource Pack: 2 Life Elixir,filler,"RESOURCE_PACK", +5287,Resource Pack: 5 Coffee,filler,"RESOURCE_PACK", +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 @@ -802,11 +896,16 @@ id,name,classification,groups,mod_name 10409,Void Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 10410,Void Salmon Sushi Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 10411,Mushroom Kebab Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul -10412,Crayfish Soup Recipe,progression,,Distant Lands - Witch Swamp Overhaul +10412,Crayfish Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul 10413,Pemmican Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul 10414,Void Mint Tea Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul -10415,Ginger Tincture Recipe,progression,GINGER_ISLAND,Distant Lands - Witch Swamp Overhaul -10416,Special Pumpkin Soup Recipe,progression,,Boarding House and Bus Stop Extension +10415,Ginger Tincture Recipe,progression,"GINGER_ISLAND,CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul +10416,Special Pumpkin Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Boarding House and Bus Stop Extension +10417,Rocky Root Coffee Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10418,Digger's Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10419,Ancient Jello Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10420,Grilled Cheese Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill +10421,Fish Casserole Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill 10450,Void Mint Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul 10451,Vile Ancient Fruit Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul 10501,Marlon's Boat Paddle,progression,GINGER_ISLAND,Stardew Valley Expanded @@ -850,10 +949,15 @@ id,name,classification,groups,mod_name 10707,Resource Pack: 5 Wooden Display,filler,RESOURCE_PACK,Archaeology 10708,Grinder,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology 10709,Ancient Battery Production Station,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology -10710,Hero Elixir,filler,RESOURCE_PACK,Starde Valley Expanded +10710,Hero Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10711,Aegis Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10712,Haste Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10713,Lightning Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10714,Armor Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10715,Gravity Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10716,Barbarian Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded +10717,Restoration Table,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology +10718,Trash Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10719,Composter,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10720,Recycling Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10721,Advanced Recycling Machine,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 68667ac5c4bf..680ddfcbacbf 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -36,6 +36,7 @@ id,region,name,tags,mod_name 35,Boiler Room,Complete Boiler Room,COMMUNITY_CENTER_ROOM, 36,Bulletin Board,Complete Bulletin Board,COMMUNITY_CENTER_ROOM, 37,Vault,Complete Vault,COMMUNITY_CENTER_ROOM, +38,Crafts Room,Forest Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", 39,Fish Tank,Deep Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE", 40,Crafts Room,Beach Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", 41,Crafts Room,Mines Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", @@ -78,7 +79,6 @@ id,region,name,tags,mod_name 78,Vault,500g Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 79,Vault,"1,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 80,Vault,"2,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", -81,Vault,"5,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 82,Vault,"1,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 83,Vault,"3,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 84,Vault,"3,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", @@ -124,6 +124,20 @@ id,region,name,tags,mod_name 124,Beach,Bamboo Pole Cutscene,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", 125,Willy's Fish Shop,Purchase Fiberglass Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", 126,Willy's Fish Shop,Purchase Iridium Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", +127,Mountain,Copper Pan Cutscene,"TOOL_UPGRADE,PAN_UPGRADE", +128,Blacksmith Iron Upgrades,Iron Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +129,Blacksmith Gold Upgrades,Gold Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +130,Blacksmith Iridium Upgrades,Iridium Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +151,Bulletin Board,Helper's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +152,Bulletin Board,Spirit's Eve Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +153,Bulletin Board,Winter Star Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +154,Bulletin Board,Calico Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +155,Pantry,Sommelier Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +156,Pantry,Dry Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +157,Fish Tank,Fish Smoker Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +158,Bulletin Board,Raccoon Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +159,Crafts Room,Green Rain Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", +160,Fish Tank,Specific Fishing Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", 201,The Mines - Floor 10,The Mines Floor 10 Treasure,"MANDATORY,THE_MINES_TREASURE", 202,The Mines - Floor 20,The Mines Floor 20 Treasure,"MANDATORY,THE_MINES_TREASURE", 203,The Mines - Floor 40,The Mines Floor 40 Treasure,"MANDATORY,THE_MINES_TREASURE", @@ -161,18 +175,18 @@ id,region,name,tags,mod_name 235,The Mines - Floor 110,Floor 110 Elevator,ELEVATOR, 236,The Mines - Floor 115,Floor 115 Elevator,ELEVATOR, 237,The Mines - Floor 120,Floor 120 Elevator,ELEVATOR, -250,Shipping,Demetrius's Breakthrough,MANDATORY +250,Shipping,Demetrius's Breakthrough,MANDATORY, 251,Volcano - Floor 10,Volcano Caldera Treasure,"GINGER_ISLAND,MANDATORY", -301,Farming,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL", -302,Farming,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL", -303,Farming,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL", -304,Farming,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL", -305,Farming,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL", -306,Farming,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL", -307,Farming,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL", -308,Farming,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL", -309,Farming,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL", -310,Farming,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL", +301,Farm,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL", +302,Farm,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL", +303,Farm,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL", +304,Farm,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL", +305,Farm,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL", +306,Farm,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL", +307,Farm,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL", +308,Farm,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL", +309,Farm,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL", +310,Farm,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL", 311,Fishing,Level 1 Fishing,"FISHING_LEVEL,SKILL_LEVEL", 312,Fishing,Level 2 Fishing,"FISHING_LEVEL,SKILL_LEVEL", 313,Fishing,Level 3 Fishing,"FISHING_LEVEL,SKILL_LEVEL", @@ -213,6 +227,11 @@ id,region,name,tags,mod_name 348,The Mines - Floor 90,Level 8 Combat,"COMBAT_LEVEL,SKILL_LEVEL", 349,The Mines - Floor 100,Level 9 Combat,"COMBAT_LEVEL,SKILL_LEVEL", 350,The Mines - Floor 110,Level 10 Combat,"COMBAT_LEVEL,SKILL_LEVEL", +351,Mastery Cave,Farming Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +352,Mastery Cave,Fishing Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +353,Mastery Cave,Foraging Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +354,Mastery Cave,Mining Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +355,Mastery Cave,Combat Mastery,"MASTERY_LEVEL,SKILL_LEVEL", 401,Carpenter Shop,Coop Blueprint,BUILDING_BLUEPRINT, 402,Carpenter Shop,Big Coop Blueprint,BUILDING_BLUEPRINT, 403,Carpenter Shop,Deluxe Coop Blueprint,BUILDING_BLUEPRINT, @@ -279,6 +298,8 @@ id,region,name,tags,mod_name 546,Mutant Bug Lair,Dark Talisman,"STORY_QUEST", 547,Witch's Swamp,Goblin Problem,"STORY_QUEST", 548,Witch's Hut,Magic Ink,"STORY_QUEST", +549,Forest,The Giant Stump,"STORY_QUEST", +550,Farm,Feeding Animals,"STORY_QUEST", 601,JotPK World 1,JotPK: Boots 1,"ARCADE_MACHINE,JOTPK", 602,JotPK World 1,JotPK: Boots 2,"ARCADE_MACHINE,JOTPK", 603,JotPK World 1,JotPK: Gun 1,"ARCADE_MACHINE,JOTPK", @@ -292,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, @@ -307,6 +328,7 @@ id,region,name,tags,mod_name 705,Farmhouse,Have Another Baby,BABY, 706,Farmhouse,Spouse Stardrop,, 707,Sewer,Krobus Stardrop,MANDATORY, +708,Forest,Pot Of Gold,MANDATORY, 801,Forest,Help Wanted: Gathering 1,HELP_WANTED, 802,Forest,Help Wanted: Gathering 2,HELP_WANTED, 803,Forest,Help Wanted: Gathering 3,HELP_WANTED, @@ -454,6 +476,7 @@ id,region,name,tags,mod_name 1068,Beach,Fishsanity: Legend II,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", 1069,Beach,Fishsanity: Ms. Angler,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", 1070,Beach,Fishsanity: Radioactive Carp,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", +1071,Fishing,Fishsanity: Goby,FISHSANITY, 1100,Museum,Museumsanity: 5 Donations,MUSEUM_MILESTONES, 1101,Museum,Museumsanity: 10 Donations,MUSEUM_MILESTONES, 1102,Museum,Museumsanity: 15 Donations,MUSEUM_MILESTONES, @@ -1021,6 +1044,57 @@ id,region,name,tags,mod_name 2034,Dance of the Moonlight Jellies,Moonlight Jellies Banner,FESTIVAL, 2035,Dance of the Moonlight Jellies,Starport Decal,FESTIVAL, 2036,Casino,Rarecrow #3 (Alien),FESTIVAL, +2041,Desert Festival,Calico Race,FESTIVAL, +2042,Desert Festival,Mummy Mask,FESTIVAL_HARD, +2043,Desert Festival,Calico Statue,FESTIVAL, +2044,Desert Festival,Emily's Outfit Services,FESTIVAL, +2045,Desert Festival,Earthy Mousse,DESERT_FESTIVAL_CHEF, +2046,Desert Festival,Sweet Bean Cake,DESERT_FESTIVAL_CHEF, +2047,Desert Festival,Skull Cave Casserole,DESERT_FESTIVAL_CHEF, +2048,Desert Festival,Spicy Tacos,DESERT_FESTIVAL_CHEF, +2049,Desert Festival,Mountain Chili,DESERT_FESTIVAL_CHEF, +2050,Desert Festival,Crystal Cake,DESERT_FESTIVAL_CHEF, +2051,Desert Festival,Cave Kebab,DESERT_FESTIVAL_CHEF, +2052,Desert Festival,Hot Log,DESERT_FESTIVAL_CHEF, +2053,Desert Festival,Sour Salad,DESERT_FESTIVAL_CHEF, +2054,Desert Festival,Superfood Cake,DESERT_FESTIVAL_CHEF, +2055,Desert Festival,Warrior Smoothie,DESERT_FESTIVAL_CHEF, +2056,Desert Festival,Rumpled Fruit Skin,DESERT_FESTIVAL_CHEF, +2057,Desert Festival,Calico Pizza,DESERT_FESTIVAL_CHEF, +2058,Desert Festival,Stuffed Mushrooms,DESERT_FESTIVAL_CHEF, +2059,Desert Festival,Elf Quesadilla,DESERT_FESTIVAL_CHEF, +2060,Desert Festival,Nachos Of The Desert,DESERT_FESTIVAL_CHEF, +2061,Desert Festival,Cioppino,DESERT_FESTIVAL_CHEF, +2062,Desert Festival,Rainforest Shrimp,DESERT_FESTIVAL_CHEF, +2063,Desert Festival,Shrimp Donut,DESERT_FESTIVAL_CHEF, +2064,Desert Festival,Smell Of The Sea,DESERT_FESTIVAL_CHEF, +2065,Desert Festival,Desert Gumbo,DESERT_FESTIVAL_CHEF, +2066,Desert Festival,Free Cactis,FESTIVAL, +2067,Desert Festival,Monster Hunt,FESTIVAL_HARD, +2068,Desert Festival,Deep Dive,FESTIVAL_HARD, +2069,Desert Festival,Treasure Hunt,FESTIVAL_HARD, +2070,Desert Festival,Touch A Calico Statue,FESTIVAL, +2071,Desert Festival,Real Calico Egg Hunter,FESTIVAL, +2072,Desert Festival,Willy's Challenge,FESTIVAL_HARD, +2073,Desert Festival,Desert Scholar,FESTIVAL, +2074,Trout Derby,Trout Derby Reward 1,FESTIVAL, +2075,Trout Derby,Trout Derby Reward 2,FESTIVAL, +2076,Trout Derby,Trout Derby Reward 3,FESTIVAL, +2077,Trout Derby,Trout Derby Reward 4,FESTIVAL_HARD, +2078,Trout Derby,Trout Derby Reward 5,FESTIVAL_HARD, +2079,Trout Derby,Trout Derby Reward 6,FESTIVAL_HARD, +2080,Trout Derby,Trout Derby Reward 7,FESTIVAL_HARD, +2081,Trout Derby,Trout Derby Reward 8,FESTIVAL_HARD, +2082,Trout Derby,Trout Derby Reward 9,FESTIVAL_HARD, +2083,Trout Derby,Trout Derby Reward 10,FESTIVAL_HARD, +2084,SquidFest,SquidFest Day 1 Copper,FESTIVAL, +2085,SquidFest,SquidFest Day 1 Iron,FESTIVAL, +2086,SquidFest,SquidFest Day 1 Gold,FESTIVAL_HARD, +2087,SquidFest,SquidFest Day 1 Iridium,FESTIVAL_HARD, +2088,SquidFest,SquidFest Day 2 Copper,FESTIVAL, +2089,SquidFest,SquidFest Day 2 Iron,FESTIVAL, +2090,SquidFest,SquidFest Day 2 Gold,FESTIVAL_HARD, +2091,SquidFest,SquidFest Day 2 Iridium,FESTIVAL_HARD, 2101,Town,Island Ingredients,"GINGER_ISLAND,SPECIAL_ORDER_BOARD", 2102,The Mines - Floor 75,Cave Patrol,SPECIAL_ORDER_BOARD, 2103,Fishing,Aquatic Overpopulation,SPECIAL_ORDER_BOARD, @@ -1065,53 +1139,59 @@ id,region,name,tags,mod_name 2214,Island West,Parrot Express,"GINGER_ISLAND,WALNUT_PURCHASE", 2215,Dig Site,Open Professor Snail Cave,GINGER_ISLAND, 2216,Field Office,Complete Island Field Office,GINGER_ISLAND, -2301,Farming,Harvest Amaranth,CROPSANITY, -2302,Farming,Harvest Artichoke,CROPSANITY, -2303,Farming,Harvest Beet,CROPSANITY, -2304,Farming,Harvest Blue Jazz,CROPSANITY, -2305,Farming,Harvest Blueberry,CROPSANITY, -2306,Farming,Harvest Bok Choy,CROPSANITY, -2307,Farming,Harvest Cauliflower,CROPSANITY, -2308,Farming,Harvest Corn,CROPSANITY, -2309,Farming,Harvest Cranberries,CROPSANITY, -2310,Farming,Harvest Eggplant,CROPSANITY, -2311,Farming,Harvest Fairy Rose,CROPSANITY, -2312,Farming,Harvest Garlic,CROPSANITY, -2313,Farming,Harvest Grape,CROPSANITY, -2314,Farming,Harvest Green Bean,CROPSANITY, -2315,Farming,Harvest Hops,CROPSANITY, -2316,Farming,Harvest Hot Pepper,CROPSANITY, -2317,Farming,Harvest Kale,CROPSANITY, -2318,Farming,Harvest Melon,CROPSANITY, -2319,Farming,Harvest Parsnip,CROPSANITY, -2320,Farming,Harvest Poppy,CROPSANITY, -2321,Farming,Harvest Potato,CROPSANITY, -2322,Farming,Harvest Pumpkin,CROPSANITY, -2323,Farming,Harvest Radish,CROPSANITY, -2324,Farming,Harvest Red Cabbage,CROPSANITY, -2325,Farming,Harvest Rhubarb,CROPSANITY, -2326,Farming,Harvest Starfruit,CROPSANITY, -2327,Farming,Harvest Strawberry,CROPSANITY, -2328,Farming,Harvest Summer Spangle,CROPSANITY, -2329,Farming,Harvest Sunflower,CROPSANITY, -2330,Farming,Harvest Tomato,CROPSANITY, -2331,Farming,Harvest Tulip,CROPSANITY, -2332,Farming,Harvest Unmilled Rice,CROPSANITY, -2333,Farming,Harvest Wheat,CROPSANITY, -2334,Farming,Harvest Yam,CROPSANITY, -2335,Farming,Harvest Cactus Fruit,CROPSANITY, -2336,Farming,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND", -2337,Farming,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND", -2338,Farming,Harvest Sweet Gem Berry,CROPSANITY, -2339,Farming,Harvest Apple,CROPSANITY, -2340,Farming,Harvest Apricot,CROPSANITY, -2341,Farming,Harvest Cherry,CROPSANITY, -2342,Farming,Harvest Orange,CROPSANITY, -2343,Farming,Harvest Pomegranate,CROPSANITY, -2344,Farming,Harvest Peach,CROPSANITY, -2345,Farming,Harvest Banana,"CROPSANITY,GINGER_ISLAND", -2346,Farming,Harvest Mango,"CROPSANITY,GINGER_ISLAND", -2347,Farming,Harvest Coffee Bean,CROPSANITY, +2301,Fall Farming,Harvest Amaranth,CROPSANITY, +2302,Fall Farming,Harvest Artichoke,CROPSANITY, +2303,Fall Farming,Harvest Beet,CROPSANITY, +2304,Spring Farming,Harvest Blue Jazz,CROPSANITY, +2305,Summer Farming,Harvest Blueberry,CROPSANITY, +2306,Fall Farming,Harvest Bok Choy,CROPSANITY, +2307,Spring Farming,Harvest Cauliflower,CROPSANITY, +2308,Summer or Fall Farming,Harvest Corn,CROPSANITY, +2309,Fall Farming,Harvest Cranberries,CROPSANITY, +2310,Fall Farming,Harvest Eggplant,CROPSANITY, +2311,Fall Farming,Harvest Fairy Rose,CROPSANITY, +2312,Spring Farming,Harvest Garlic,CROPSANITY, +2313,Fall Farming,Harvest Grape,CROPSANITY, +2314,Spring Farming,Harvest Green Bean,CROPSANITY, +2315,Summer Farming,Harvest Hops,CROPSANITY, +2316,Summer Farming,Harvest Hot Pepper,CROPSANITY, +2317,Spring Farming,Harvest Kale,CROPSANITY, +2318,Summer Farming,Harvest Melon,CROPSANITY, +2319,Spring Farming,Harvest Parsnip,CROPSANITY, +2320,Summer Farming,Harvest Poppy,CROPSANITY, +2321,Spring Farming,Harvest Potato,CROPSANITY, +2322,Fall Farming,Harvest Pumpkin,CROPSANITY, +2323,Summer Farming,Harvest Radish,CROPSANITY, +2324,Summer Farming,Harvest Red Cabbage,CROPSANITY, +2325,Spring Farming,Harvest Rhubarb,CROPSANITY, +2326,Summer Farming,Harvest Starfruit,CROPSANITY, +2327,Spring Farming,Harvest Strawberry,CROPSANITY, +2328,Summer Farming,Harvest Summer Spangle,CROPSANITY, +2329,Summer or Fall Farming,Harvest Sunflower,CROPSANITY, +2330,Summer Farming,Harvest Tomato,CROPSANITY, +2331,Spring Farming,Harvest Tulip,CROPSANITY, +2332,Spring Farming,Harvest Unmilled Rice,CROPSANITY, +2333,Summer or Fall Farming,Harvest Wheat,CROPSANITY, +2334,Fall Farming,Harvest Yam,CROPSANITY, +2335,Indoor Farming,Harvest Cactus Fruit,CROPSANITY, +2336,Summer Farming,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND", +2337,Summer Farming,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND", +2338,Fall Farming,Harvest Sweet Gem Berry,CROPSANITY, +2339,Fall Farming,Harvest Apple,CROPSANITY, +2340,Spring Farming,Harvest Apricot,CROPSANITY, +2341,Spring Farming,Harvest Cherry,CROPSANITY, +2342,Summer Farming,Harvest Orange,CROPSANITY, +2343,Fall Farming,Harvest Pomegranate,CROPSANITY, +2344,Summer Farming,Harvest Peach,CROPSANITY, +2345,Summer Farming,Harvest Banana,"CROPSANITY,GINGER_ISLAND", +2346,Summer Farming,Harvest Mango,"CROPSANITY,GINGER_ISLAND", +2347,Indoor Farming,Harvest Coffee Bean,CROPSANITY, +2348,Fall Farming,Harvest Broccoli,CROPSANITY, +2349,Spring Farming,Harvest Carrot,CROPSANITY, +2350,Summer Farming,Harvest Powdermelon,CROPSANITY, +2351,Summer Farming,Harvest Summer Squash,CROPSANITY, +2352,Indoor Farming,Harvest Ancient Fruit,CROPSANITY, +2353,Indoor Farming,Harvest Qi Fruit,"CROPSANITY,GINGER_ISLAND", 2401,Shipping,Shipsanity: Duck Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 2402,Shipping,Shipsanity: Duck Feather,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 2403,Shipping,Shipsanity: Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", @@ -1431,7 +1511,7 @@ id,region,name,tags,mod_name 2717,Shipping,Shipsanity: Cactus Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2718,Shipping,Shipsanity: Cave Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2719,Shipping,Shipsanity: Chanterelle,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", -2720,Shipping,Shipsanity: Clam,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +2720,Shipping,Shipsanity: Clam,"SHIPSANITY,SHIPSANITY_FISH", 2721,Shipping,Shipsanity: Coconut,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2722,Shipping,Shipsanity: Common Mushroom,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2723,Shipping,Shipsanity: Coral,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", @@ -1683,7 +1763,7 @@ id,region,name,tags,mod_name 2969,Shipping,Shipsanity: Mango Sapling,"GINGER_ISLAND,SHIPSANITY", 2970,Shipping,Shipsanity: Mushroom Tree Seed,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS", 2971,Shipping,Shipsanity: Pineapple Seeds,"GINGER_ISLAND,SHIPSANITY", -2972,Shipping,Shipsanity: Qi Bean,"GINGER_ISLAND,SHIPSANITY", +2972,Shipping,Shipsanity: Qi Bean,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS", 2973,Shipping,Shipsanity: Taro Tuber,"GINGER_ISLAND,SHIPSANITY", 3001,Adventurer's Guild,Monster Eradication: Slimes,"MONSTERSANITY,MONSTERSANITY_GOALS", 3002,Adventurer's Guild,Monster Eradication: Void Spirits,"MONSTERSANITY,MONSTERSANITY_GOALS", @@ -1852,6 +1932,7 @@ id,region,name,tags,mod_name 3278,Kitchen,Cook Tropical Curry,"COOKSANITY,GINGER_ISLAND", 3279,Kitchen,Cook Trout Soup,"COOKSANITY,COOKSANITY_QOS", 3280,Kitchen,Cook Vegetable Medley,COOKSANITY, +3281,Kitchen,Cook Moss Soup,COOKSANITY, 3301,Farm,Algae Soup Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", 3302,The Queen of Sauce,Artichoke Dip Recipe,"CHEFSANITY,CHEFSANITY_QOS", 3303,Farm,Autumn's Bounty Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", @@ -1932,6 +2013,7 @@ id,region,name,tags,mod_name 3378,Island Resort,Tropical Curry Recipe,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE", 3379,The Queen of Sauce,Trout Soup Recipe,"CHEFSANITY,CHEFSANITY_QOS", 3380,Farm,Vegetable Medley Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", +3381,Farm,Moss Soup Recipe,"CHEFSANITY,CHEFSANITY_SKILL", 3401,Farm,Craft Cherry Bomb,CRAFTSANITY, 3402,Farm,Craft Bomb,CRAFTSANITY, 3403,Farm,Craft Mega Bomb,CRAFTSANITY, @@ -2006,7 +2088,7 @@ id,region,name,tags,mod_name 3472,Farm,Craft Life Elixir,CRAFTSANITY, 3473,Farm,Craft Oil of Garlic,CRAFTSANITY, 3474,Farm,Craft Monster Musk,CRAFTSANITY, -3475,Farm,Craft Fairy Dust,CRAFTSANITY, +3475,Farm,Craft Fairy Dust,"CRAFTSANITY,GINGER_ISLAND", 3476,Farm,Craft Warp Totem: Beach,CRAFTSANITY, 3477,Farm,Craft Warp Totem: Mountains,CRAFTSANITY, 3478,Farm,Craft Warp Totem: Farm,CRAFTSANITY, @@ -2062,6 +2144,26 @@ id,region,name,tags,mod_name 3528,Farm,Craft Farm Computer,CRAFTSANITY, 3529,Farm,Craft Hopper,"CRAFTSANITY,GINGER_ISLAND", 3530,Farm,Craft Cookout Kit,CRAFTSANITY, +3531,Farm,Craft Fish Smoker,"CRAFTSANITY", +3532,Farm,Craft Dehydrator,"CRAFTSANITY", +3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,GINGER_ISLAND", +3534,Farm,Craft Mystic Tree Seed,"CRAFTSANITY,REQUIRES_MASTERIES", +3535,Farm,Craft Sonar Bobber,"CRAFTSANITY", +3536,Farm,Craft Challenge Bait,"CRAFTSANITY,REQUIRES_MASTERIES", +3537,Farm,Craft Treasure Totem,"CRAFTSANITY,REQUIRES_MASTERIES", +3538,Farm,Craft Heavy Furnace,"CRAFTSANITY,REQUIRES_MASTERIES", +3539,Farm,Craft Deluxe Worm Bin,"CRAFTSANITY", +3540,Farm,Craft Mushroom Log,"CRAFTSANITY", +3541,Farm,Craft Big Chest,"CRAFTSANITY", +3542,Farm,Craft Big Stone Chest,"CRAFTSANITY", +3543,Farm,Craft Text Sign,"CRAFTSANITY", +3544,Farm,Craft Tent Kit,"CRAFTSANITY", +3545,Farm,Craft Statue Of The Dwarf King,"CRAFTSANITY,REQUIRES_MASTERIES", +3546,Farm,Craft Statue Of Blessings,"CRAFTSANITY,REQUIRES_MASTERIES", +3547,Farm,Craft Anvil,"CRAFTSANITY,REQUIRES_MASTERIES", +3548,Farm,Craft Mini-Forge,"CRAFTSANITY,GINGER_ISLAND,REQUIRES_MASTERIES", +3549,Farm,Craft Deluxe Bait,"CRAFTSANITY", +3550,Farm,Craft Bait Maker,"CRAFTSANITY", 3551,Pierre's General Store,Grass Starter Recipe,CRAFTSANITY, 3552,Carpenter Shop,Wood Floor Recipe,CRAFTSANITY, 3553,Carpenter Shop,Rustic Plank Floor Recipe,CRAFTSANITY, @@ -2088,6 +2190,226 @@ id,region,name,tags,mod_name 3574,Sewer,Wicked Statue Recipe,CRAFTSANITY, 3575,Desert,Warp Totem: Desert Recipe,"CRAFTSANITY", 3576,Island Trader,Deluxe Retaining Soil Recipe,"CRAFTSANITY,GINGER_ISLAND", +3577,Willy's Fish Shop,Fish Smoker Recipe,CRAFTSANITY, +3578,Pierre's General Store,Dehydrator Recipe,CRAFTSANITY, +3579,Carpenter Shop,Big Chest Recipe,CRAFTSANITY, +3580,Mines Dwarf Shop,Big Stone Chest Recipe,CRAFTSANITY, +3701,Raccoon Bundles,Raccoon Request 1,"BUNDLE,RACCOON_BUNDLES", +3702,Raccoon Bundles,Raccoon Request 2,"BUNDLE,RACCOON_BUNDLES", +3703,Raccoon Bundles,Raccoon Request 3,"BUNDLE,RACCOON_BUNDLES", +3704,Raccoon Bundles,Raccoon Request 4,"BUNDLE,RACCOON_BUNDLES", +3705,Raccoon Bundles,Raccoon Request 5,"BUNDLE,RACCOON_BUNDLES", +3706,Raccoon Bundles,Raccoon Request 6,"BUNDLE,RACCOON_BUNDLES", +3707,Raccoon Bundles,Raccoon Request 7,"BUNDLE,RACCOON_BUNDLES", +3708,Raccoon Bundles,Raccoon Request 8,"BUNDLE,RACCOON_BUNDLES", +3801,Shipping,Shipsanity: Goby,"SHIPSANITY,SHIPSANITY_FISH", +3802,Shipping,Shipsanity: Fireworks (Red),"SHIPSANITY", +3803,Shipping,Shipsanity: Fireworks (Purple),"SHIPSANITY", +3804,Shipping,Shipsanity: Fireworks (Green),"SHIPSANITY", +3805,Shipping,Shipsanity: Far Away Stone,"SHIPSANITY", +3806,Shipping,Shipsanity: Calico Egg,"SHIPSANITY", +3807,Shipping,Shipsanity: Mixed Flower Seeds,"SHIPSANITY", +3808,Shipping,Shipsanity: Mystery Box,"SHIPSANITY", +3809,Shipping,Shipsanity: Golden Tag,"SHIPSANITY", +3810,Shipping,Shipsanity: Deluxe Bait,"SHIPSANITY", +3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY", +3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY", +3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY", +3815,Shipping,Shipsanity: Mystic Tree Seed,"SHIPSANITY,REQUIRES_MASTERIES", +3816,Shipping,Shipsanity: Mystic Syrup,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3817,Shipping,Shipsanity: Raisins,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3818,Shipping,Shipsanity: Dried Fruit,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3819,Shipping,Shipsanity: Dried Mushrooms,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY", +3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY", +3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES", +3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES", +3824,Shipping,Shipsanity: Carrot Seeds,"SHIPSANITY", +3825,Shipping,Shipsanity: Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3826,Shipping,Shipsanity: Summer Squash Seeds,"SHIPSANITY", +3827,Shipping,Shipsanity: Summer Squash,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3828,Shipping,Shipsanity: Broccoli Seeds,"SHIPSANITY", +3829,Shipping,Shipsanity: Broccoli,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3830,Shipping,Shipsanity: Powdermelon Seeds,"SHIPSANITY", +3831,Shipping,Shipsanity: Powdermelon,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3832,Shipping,Shipsanity: Smoked Fish,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3833,Shipping,Shipsanity: Book Of Stars,"SHIPSANITY", +3834,Shipping,Shipsanity: Stardew Valley Almanac,"SHIPSANITY", +3835,Shipping,Shipsanity: Woodcutter's Weekly,"SHIPSANITY", +3836,Shipping,Shipsanity: Bait And Bobber,"SHIPSANITY", +3837,Shipping,Shipsanity: Mining Monthly,"SHIPSANITY", +3838,Shipping,Shipsanity: Combat Quarterly,"SHIPSANITY", +3839,Shipping,Shipsanity: The Alleyway Buffet,"SHIPSANITY", +3840,Shipping,Shipsanity: The Art O' Crabbing,"SHIPSANITY", +3841,Shipping,Shipsanity: Dwarvish Safety Manual,"SHIPSANITY", +3842,Shipping,Shipsanity: Jewels Of The Sea,"SHIPSANITY", +3843,Shipping,Shipsanity: Raccoon Journal,"SHIPSANITY", +3844,Shipping,Shipsanity: Woody's Secret,"SHIPSANITY", +3845,Shipping,"Shipsanity: Jack Be Nimble, Jack Be Thick","SHIPSANITY", +3846,Shipping,Shipsanity: Friendship 101,"SHIPSANITY", +3847,Shipping,Shipsanity: Monster Compendium,"SHIPSANITY", +3848,Shipping,Shipsanity: Way Of The Wind pt. 1,"SHIPSANITY", +3849,Shipping,Shipsanity: Mapping Cave Systems,"SHIPSANITY", +3850,Shipping,Shipsanity: Price Catalogue,"SHIPSANITY", +3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY,GINGER_ISLAND", +3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND", +3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY", +3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY", +3855,Shipping,Shipsanity: Way Of The Wind pt. 2,"SHIPSANITY", +3856,Shipping,Shipsanity: Golden Animal Cracker,"SHIPSANITY,REQUIRES_MASTERIES", +3857,Shipping,Shipsanity: Golden Mystery Box,"SHIPSANITY,REQUIRES_MASTERIES", +3858,Shipping,Shipsanity: Sea Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3859,Shipping,Shipsanity: Cave Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3860,Shipping,Shipsanity: River Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3861,Shipping,Shipsanity: Treasure Appraisal Guide,"SHIPSANITY", +3862,Shipping,Shipsanity: Horse: The Book,"SHIPSANITY", +3863,Shipping,Shipsanity: Butterfly Powder,"SHIPSANITY", +3864,Shipping,Shipsanity: Blue Grass Starter,"SHIPSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", +3865,Shipping,Shipsanity: Moss Soup,"SHIPSANITY", +3866,Shipping,Shipsanity: Ol' Slitherlegs,"SHIPSANITY", +3867,Shipping,Shipsanity: Targeted Bait,"SHIPSANITY", +4001,Farm,Read Price Catalogue,"BOOKSANITY,BOOKSANITY_POWER", +4002,Farm,Read Mapping Cave Systems,"BOOKSANITY,BOOKSANITY_POWER", +4003,Farm,Read Way Of The Wind pt. 1,"BOOKSANITY,BOOKSANITY_POWER", +4004,Farm,Read Way Of The Wind pt. 2,"BOOKSANITY,BOOKSANITY_POWER", +4005,Farm,Read Monster Compendium,"BOOKSANITY,BOOKSANITY_POWER", +4006,Farm,Read Friendship 101,"BOOKSANITY,BOOKSANITY_POWER", +4007,Farm,"Read Jack Be Nimble, Jack Be Thick","BOOKSANITY,BOOKSANITY_POWER", +4008,Farm,Read Woody's Secret,"BOOKSANITY,BOOKSANITY_POWER", +4009,Farm,Read Raccoon Journal,"BOOKSANITY,BOOKSANITY_POWER", +4010,Farm,Read Jewels Of The Sea,"BOOKSANITY,BOOKSANITY_POWER", +4011,Farm,Read Dwarvish Safety Manual,"BOOKSANITY,BOOKSANITY_POWER", +4012,Farm,Read The Art O' Crabbing,"BOOKSANITY,BOOKSANITY_POWER", +4013,Farm,Read The Alleyway Buffet,"BOOKSANITY,BOOKSANITY_POWER", +4014,Farm,Read The Diamond Hunter,"BOOKSANITY,BOOKSANITY_POWER,GINGER_ISLAND", +4015,Farm,Read Book of Mysteries,"BOOKSANITY,BOOKSANITY_POWER", +4016,Farm,Read Horse: The Book,"BOOKSANITY,BOOKSANITY_POWER", +4017,Farm,Read Treasure Appraisal Guide,"BOOKSANITY,BOOKSANITY_POWER", +4018,Farm,Read Ol' Slitherlegs,"BOOKSANITY,BOOKSANITY_POWER", +4019,Farm,Read Animal Catalogue,"BOOKSANITY,BOOKSANITY_POWER", +4031,Farm,Read Bait And Bobber,"BOOKSANITY,BOOKSANITY_SKILL", +4032,Farm,Read Book Of Stars,"BOOKSANITY,BOOKSANITY_SKILL", +4033,Farm,Read Combat Quarterly,"BOOKSANITY,BOOKSANITY_SKILL", +4034,Farm,Read Mining Monthly,"BOOKSANITY,BOOKSANITY_SKILL", +4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL,GINGER_ISLAND", +4036,Farm,Read Stardew Valley Almanac,"BOOKSANITY,BOOKSANITY_SKILL", +4037,Farm,Read Woodcutter's Weekly,"BOOKSANITY,BOOKSANITY_SKILL", +4051,Museum,Read Tips on Farming,"BOOKSANITY,BOOKSANITY_LOST", +4052,Museum,Read This is a book by Marnie,"BOOKSANITY,BOOKSANITY_LOST", +4053,Museum,Read On Foraging,"BOOKSANITY,BOOKSANITY_LOST", +4054,Museum,"Read The Fisherman, Act 1","BOOKSANITY,BOOKSANITY_LOST", +4055,Museum,Read How Deep do the mines go?,"BOOKSANITY,BOOKSANITY_LOST", +4056,Museum,Read An Old Farmer's Journal,"BOOKSANITY,BOOKSANITY_LOST", +4057,Museum,Read Scarecrows,"BOOKSANITY,BOOKSANITY_LOST", +4058,Museum,Read The Secret of the Stardrop,"BOOKSANITY,BOOKSANITY_LOST", +4059,Museum,Read Journey of the Prairie King -- The Smash Hit Video Game!,"BOOKSANITY,BOOKSANITY_LOST", +4060,Museum,Read A Study on Diamond Yields,"BOOKSANITY,BOOKSANITY_LOST", +4061,Museum,Read Brewmaster's Guide,"BOOKSANITY,BOOKSANITY_LOST", +4062,Museum,Read Mysteries of the Dwarves,"BOOKSANITY,BOOKSANITY_LOST", +4063,Museum,Read Highlights From The Book of Yoba,"BOOKSANITY,BOOKSANITY_LOST", +4064,Museum,Read Marriage Guide for Farmers,"BOOKSANITY,BOOKSANITY_LOST", +4065,Museum,"Read The Fisherman, Act II","BOOKSANITY,BOOKSANITY_LOST", +4066,Museum,Read Technology Report!,"BOOKSANITY,BOOKSANITY_LOST", +4067,Museum,Read Secrets of the Legendary Fish,"BOOKSANITY,BOOKSANITY_LOST", +4068,Museum,Read Gunther Tunnel Notice,"BOOKSANITY,BOOKSANITY_LOST", +4069,Museum,Read Note From Gunther,"BOOKSANITY,BOOKSANITY_LOST", +4070,Museum,Read Goblins by M. Jasper,"BOOKSANITY,BOOKSANITY_LOST", +4071,Museum,Read Secret Statues Acrostics,"BOOKSANITY,BOOKSANITY_LOST", +4101,Clint's Blacksmith,Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4102,Island West,Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4103,Island West,Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4104,Island North,Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4105,Island North,Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4106,Island Southeast,Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4107,Island East,Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4108,Island East,Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4109,Leo's Hut,Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4110,Island Shrine,Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4111,Island Shrine,Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4112,Island West,Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4113,Island West,Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4114,Island West,Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4115,Island West,Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4116,Island West,Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4117,Gourmand Frog Cave,Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4118,Gourmand Frog Cave,Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4119,Gourmand Frog Cave,Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4120,Island West,Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG", +4121,Island West,Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4122,Island West,Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4123,Island West,Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4124,Island West,Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4125,Island West,Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4126,Shipwreck,Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4127,Island West,Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4128,Island West,Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG", +4129,Island West,Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG", +4130,Island West,X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4131,Island West,Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG", +4132,Island West,Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4133,Island West,Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG", +4134,Island West,Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4135,Island West,Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4136,Island West,Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4137,Island West,Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4138,Island West,Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4139,Island West,Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4140,Colored Crystals Cave,Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4141,Island West,Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4142,Island West,Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG", +4143,Island West,Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4144,Island West,Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4145,Island North,Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4146,Island North,Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4147,Island North,Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4148,Island North,Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4149,Island North,Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4150,Dig Site,Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4151,Dig Site,Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4152,Dig Site,Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4153,Dig Site,Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4154,Field Office,Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4155,Field Office,Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4156,Field Office,Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4157,Field Office,Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4158,Field Office,Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4159,Field Office,Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4160,Island North,Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4161,Island North,Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4162,Island North,Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4163,Island North,Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG", +4164,Island North,Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4165,Island North,Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4166,Volcano Secret Beach,Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4167,Volcano Secret Beach,Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4168,Volcano - Floor 5,Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4169,Volcano - Floor 5,Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4170,Volcano - Floor 10,Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4171,Volcano - Floor 10,Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4172,Volcano - Floor 10,Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4173,Volcano - Floor 5,Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4174,Volcano - Floor 5,Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4175,Volcano - Floor 10,Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4176,Volcano - Floor 10,Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4177,Volcano - Floor 10,Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4178,Volcano - Floor 5,Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4179,Volcano - Floor 5,Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4180,Volcano - Floor 10,Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4181,Volcano - Floor 10,Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4182,Volcano - Floor 10,Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4183,Volcano - Floor 5,Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4184,Volcano - Floor 10,Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4185,Volcano - Floor 10,Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4186,Volcano - Floor 10,Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4187,Island North,Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4188,Island Southeast,Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4189,Island Southeast,Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG", +4190,Island Southeast,Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4191,Pirate Cove,Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4192,Pirate Cove,Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4193,Pirate Cove,Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4194,Pirate Cove,Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", 5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill @@ -2585,7 +2907,7 @@ id,region,name,tags,mod_name 7405,Farm,Craft Armor Elixir,CRAFTSANITY,Stardew Valley Expanded 7406,Witch's Swamp,Craft Ginger Tincture,"CRAFTSANITY,GINGER_ISLAND",Distant Lands - Witch Swamp Overhaul 7407,Farm,Craft Glass Path,CRAFTSANITY,Archaeology -7408,Farm,Craft Glass Bazier,CRAFTSANITY,Archaeology +7408,Farm,Craft Glass Brazier,CRAFTSANITY,Archaeology 7409,Farm,Craft Glass Fence,CRAFTSANITY,Archaeology 7410,Farm,Craft Bone Path,CRAFTSANITY,Archaeology 7411,Farm,Craft Water Shifter,CRAFTSANITY,Archaeology @@ -2603,13 +2925,23 @@ id,region,name,tags,mod_name 7423,Farm,Craft T-Rex Skeleton L,CRAFTSANITY,Boarding House and Bus Stop Extension 7424,Farm,Craft T-Rex Skeleton M,CRAFTSANITY,Boarding House and Bus Stop Extension 7425,Farm,Craft T-Rex Skeleton R,CRAFTSANITY,Boarding House and Bus Stop Extension +7426,Farm,Craft Restoration Table,CRAFTSANITY,Archaeology +7427,Farm,Craft Rusty Path,CRAFTSANITY,Archaeology +7428,Farm,Craft Rusty Brazier,CRAFTSANITY,Archaeology +7429,Farm,Craft Lucky Ring,CRAFTSANITY,Archaeology +7430,Farm,Craft Bone Fence,CRAFTSANITY,Archaeology +7431,Farm,Craft Bouquet,CRAFTSANITY,Socializing Skill +7432,Farm,Craft Trash Bin,CRAFTSANITY,Binning Skill +7433,Farm,Craft Composter,CRAFTSANITY,Binning Skill +7434,Farm,Craft Recycling Bin,CRAFTSANITY,Binning Skill +7435,Farm,Craft Advanced Recycling Machine,CRAFTSANITY,Binning Skill 7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic 7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic 7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7454,Isaac Shop,Hero Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7455,Alesia Shop,Armor Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7501,Mountain,Missing Envelope,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) -7502,Forest,Lost Emerald Ring,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) +7502,Forest,Ayeisha's Lost Ring,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) 7503,Forest,Mr.Ginger's request,"STORY_QUEST",Mister Ginger (cat npc) 7504,Forest,Juna's Drink Request,"STORY_QUEST",Juna - Roommate NPC 7505,Forest,Juna's BFF Request,"STORY_QUEST",Juna - Roommate NPC @@ -2648,6 +2980,11 @@ id,region,name,tags,mod_name 7563,Kitchen,Cook Pemmican,COOKSANITY,Distant Lands - Witch Swamp Overhaul 7564,Kitchen,Cook Void Mint Tea,COOKSANITY,Distant Lands - Witch Swamp Overhaul 7565,Kitchen,Cook Special Pumpkin Soup,COOKSANITY,Boarding House and Bus Stop Extension +7566,Kitchen,Cook Digger's Delight,COOKSANITY,Archaeology +7567,Kitchen,Cook Rocky Root Coffee,COOKSANITY,Archaeology +7568,Kitchen,Cook Ancient Jello,COOKSANITY,Archaeology +7569,Kitchen,Cook Grilled Cheese,COOKSANITY,Binning Skill +7570,Kitchen,Cook Fish Casserole,COOKSANITY,Binning Skill 7601,Bear Shop,Baked Berry Oatmeal Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 7602,Bear Shop,Flower Cookie Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 7603,Saloon,Big Bark Burger Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Stardew Valley Expanded @@ -2668,6 +3005,11 @@ id,region,name,tags,mod_name 7620,Mines Dwarf Shop,T-Rex Skeleton L Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension 7621,Mines Dwarf Shop,T-Rex Skeleton M Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension 7622,Mines Dwarf Shop,T-Rex Skeleton R Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension +7623,Farm,Digger's Delight Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7624,Farm,Rocky Root Coffee Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7625,Farm,Ancient Jello Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7627,Farm,Grilled Cheese Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill +7628,Farm,Fish Casserole Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill 7651,Alesia Shop,Tempered Galaxy Dagger,MANDATORY,Stardew Valley Expanded 7652,Isaac Shop,Tempered Galaxy Sword,MANDATORY,Stardew Valley Expanded 7653,Isaac Shop,Tempered Galaxy Hammer,MANDATORY,Stardew Valley Expanded @@ -2697,7 +3039,6 @@ id,region,name,tags,mod_name 7724,Mutant Bug Lair,Fishsanity: Water Grub,FISHSANITY,Stardew Valley Expanded 7725,Crimson Badlands,Fishsanity: Undeadfish,FISHSANITY,Stardew Valley Expanded 7726,Shearwater Bridge,Fishsanity: Kittyfish,FISHSANITY,Stardew Valley Expanded -7727,Blue Moon Vineyard,Fishsanity: Dulse Seaweed,FISHSANITY,Stardew Valley Expanded 7728,Witch's Swamp,Fishsanity: Void Minnow,FISHSANITY,Distant Lands - Witch Swamp Overhaul 7729,Witch's Swamp,Fishsanity: Swamp Leech,FISHSANITY,Distant Lands - Witch Swamp Overhaul 7730,Witch's Swamp,Fishsanity: Giant Horsehoe Crab,FISHSANITY,Distant Lands - Witch Swamp Overhaul @@ -2714,15 +3055,15 @@ id,region,name,tags,mod_name 8002,Shipping,Shipsanity: Travel Core,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Magic 8003,Shipping,Shipsanity: Aegis Elixir,SHIPSANITY,Stardew Valley Expanded 8004,Shipping,Shipsanity: Aged Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded -8005,Shipping,Shipsanity: Ancient Ferns Seed,SHIPSANITY,Stardew Valley Expanded +8005,Shipping,Shipsanity: Ancient Fern Seed,SHIPSANITY,Stardew Valley Expanded 8006,Shipping,Shipsanity: Ancient Fiber,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8007,Shipping,Shipsanity: Armor Elixir,SHIPSANITY,Stardew Valley Expanded 8008,Shipping,Shipsanity: Baby Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8009,Shipping,Shipsanity: Baked Berry Oatmeal,SHIPSANITY,Stardew Valley Expanded 8010,Shipping,Shipsanity: Barbarian Elixir,SHIPSANITY,Stardew Valley Expanded -8011,Shipping,Shipsanity: Bearberrys,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8011,Shipping,Shipsanity: Bearberry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8012,Shipping,Shipsanity: Big Bark Burger,SHIPSANITY,Stardew Valley Expanded -8013,Shipping,Shipsanity: Big Conch,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8013,Shipping,Shipsanity: Conch,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8014,Shipping,Shipsanity: Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded 8015,Shipping,Shipsanity: Bonefish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8016,Shipping,Shipsanity: Bull Trout,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2730,8 +3071,7 @@ id,region,name,tags,mod_name 8018,Shipping,Shipsanity: Clownfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8019,Shipping,Shipsanity: Daggerfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8020,Shipping,Shipsanity: Dewdrop Berry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded -8021,Shipping,Shipsanity: Dried Sand Dollar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded -8022,Shipping,Shipsanity: Dulse Seaweed,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded +8021,Shipping,Shipsanity: Sand Dollar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8023,Shipping,Shipsanity: Ferngill Primrose,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8024,Shipping,Shipsanity: Flower Cookie,SHIPSANITY,Stardew Valley Expanded 8025,Shipping,Shipsanity: Frog,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2751,7 +3091,7 @@ id,region,name,tags,mod_name 8040,Shipping,Shipsanity: King Salmon,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8050,Shipping,Shipsanity: Kittyfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8051,Shipping,Shipsanity: Lightning Elixir,SHIPSANITY,Stardew Valley Expanded -8052,Shipping,Shipsanity: Lucky Four Leaf Clover,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8052,Shipping,Shipsanity: Four Leaf Clover,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8053,Shipping,Shipsanity: Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8054,Shipping,Shipsanity: Meteor Carp,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8055,Shipping,Shipsanity: Minnow,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2774,7 +3114,7 @@ id,region,name,tags,mod_name 8072,Shipping,Shipsanity: Shrub Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded 8073,Shipping,Shipsanity: Slime Berry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded 8074,Shipping,Shipsanity: Slime Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded -8075,Shipping,Shipsanity: Smelly Rafflesia,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8075,Shipping,Shipsanity: Rafflesia,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8076,Shipping,Shipsanity: Sports Drink,SHIPSANITY,Stardew Valley Expanded 8077,Shipping,Shipsanity: Stalk Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded 8078,Shipping,Shipsanity: Stamina Capsule,SHIPSANITY,Stardew Valley Expanded @@ -2937,3 +3277,12 @@ id,region,name,tags,mod_name 8235,Shipping,Shipsanity: Pterodactyl Claw,SHIPSANITY,Boarding House and Bus Stop Extension 8236,Shipping,Shipsanity: Neanderthal Skull,SHIPSANITY,Boarding House and Bus Stop Extension 8237,Shipping,Shipsanity: Pterodactyl R Wing Bone,SHIPSANITY,Boarding House and Bus Stop Extension +8238,Shipping,Shipsanity: Scrap Rust,SHIPSANITY,Archaeology +8239,Shipping,Shipsanity: Rusty Path,SHIPSANITY,Archaeology +8241,Shipping,Shipsanity: Digger's Delight,SHIPSANITY,Archaeology +8242,Shipping,Shipsanity: Rocky Root Coffee,SHIPSANITY,Archaeology +8243,Shipping,Shipsanity: Ancient Jello,SHIPSANITY,Archaeology +8244,Shipping,Shipsanity: Bone Fence,SHIPSANITY,Archaeology +8245,Shipping,Shipsanity: Grilled Cheese,SHIPSANITY,Binning Skill +8246,Shipping,Shipsanity: Fish Casserole,SHIPSANITY,Binning Skill +8247,Shipping,Shipsanity: Snatcher Worm,SHIPSANITY,Stardew Valley Expanded diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py index 544bb92e6e55..b81c518a37c9 100644 --- a/worlds/stardew_valley/data/museum_data.py +++ b/worlds/stardew_valley/data/museum_data.py @@ -76,6 +76,8 @@ def create_mineral(name: str, difficulty += 1.0 / 26.0 * 100 if "Omni Geode" in geodes: difficulty += 31.0 / 2750.0 * 100 + if "Fishing Chest" in geodes: + difficulty += 4.3 mineral_item = MuseumItem.of(name, difficulty, locations, geodes, monsters) all_museum_minerals.append(mineral_item) @@ -95,7 +97,7 @@ class Artifact: geodes=Geode.artifact_trove) arrowhead = create_artifact("Arrowhead", 8.5, (Region.mountain, Region.forest, Region.bus_stop), geodes=Geode.artifact_trove) - ancient_doll = create_artifact("Ancient Doll", 13.1, (Region.mountain, Region.forest, Region.bus_stop), + ancient_doll = create_artifact(Artifact.ancient_doll, 13.1, (Region.mountain, Region.forest, Region.bus_stop), geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) elvish_jewelry = create_artifact("Elvish Jewelry", 5.3, Region.forest, geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) @@ -103,8 +105,7 @@ class Artifact: geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town), geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) - dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.mountain, Region.skull_cavern), - geodes=WaterChest.fishing_chest, + dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.skull_cavern), monsters=Monster.pepper_rex) rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley, geodes=(Geode.artifact_trove, WaterChest.fishing_chest), @@ -170,18 +171,18 @@ class Artifact: class Mineral: - quartz = create_mineral(Mineral.quartz, Region.mines_floor_20) + quartz = create_mineral(Mineral.quartz, Region.mines_floor_20, difficulty=100.0 / 5.0) fire_quartz = create_mineral("Fire Quartz", Region.mines_floor_100, geodes=(Geode.magma, Geode.omni, WaterChest.fishing_chest), - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) frozen_tear = create_mineral("Frozen Tear", Region.mines_floor_60, geodes=(Geode.frozen, Geode.omni, WaterChest.fishing_chest), monsters=unlikely, - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) earth_crystal = create_mineral("Earth Crystal", Region.mines_floor_20, geodes=(Geode.geode, Geode.omni, WaterChest.fishing_chest), monsters=Monster.duggy, - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) emerald = create_mineral("Emerald", Region.mines_floor_100, geodes=WaterChest.fishing_chest) aquamarine = create_mineral("Aquamarine", Region.mines_floor_60, diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py index 62dcd8709c64..3123bb924307 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -5,17 +5,18 @@ from ..strings.artisan_good_names import ArtisanGood from ..strings.craftable_names import ModEdible, Edible from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop -from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish +from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem from ..strings.flower_names import Flower -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable +from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom from ..strings.ingredient_names import Ingredient -from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal +from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal, ArchaeologyMeal, TrashyMeal from ..strings.material_names import Material -from ..strings.metal_names import Fossil +from ..strings.metal_names import Fossil, Artifact from ..strings.monster_drop_names import Loot from ..strings.region_names import Region, SVERegion from ..strings.season_names import Season -from ..strings.skill_names import Skill +from ..strings.seed_names import Seed +from ..strings.skill_names import Skill, ModSkill from ..strings.villager_names import NPC, ModNPC @@ -49,9 +50,9 @@ def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, return create_recipe(name, ingredients, source, mod_name) -def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int]) -> CookingRecipe: +def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe: source = SkillSource(skill, level) - return create_recipe(name, ingredients, source) + return create_recipe(name, ingredients, source, mod_name) def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe: @@ -116,7 +117,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, fried_calamari = friendship_recipe(Meal.fried_calamari, NPC.jodi, 3, {Fish.squid: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}) fried_eel = friendship_recipe(Meal.fried_eel, NPC.george, 3, {Fish.eel: 1, Ingredient.oil: 1}) fried_egg = starter_recipe(Meal.fried_egg, {AnimalProduct.chicken_egg: 1}) -fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Forageable.common_mushroom: 1, Forageable.morel: 1, Ingredient.oil: 1}) +fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Mushroom.common: 1, Mushroom.morel: 1, Ingredient.oil: 1}) fruit_salad = queen_of_sauce_recipe(Meal.fruit_salad, 2, Season.fall, 7, {Fruit.blueberry: 1, Fruit.melon: 1, Fruit.apricot: 1}) ginger_ale = shop_recipe(Beverage.ginger_ale, Region.volcano_dwarf_shop, 1000, {Forageable.ginger: 3, Ingredient.sugar: 1}) glazed_yams = queen_of_sauce_recipe(Meal.glazed_yams, 1, Season.fall, 21, {Vegetable.yam: 1, Ingredient.sugar: 1}) @@ -130,6 +131,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, mango_sticky_rice = friendship_recipe(Meal.mango_sticky_rice, NPC.leo, 7, {Fruit.mango: 1, Forageable.coconut: 1, Ingredient.rice: 1}) maple_bar = queen_of_sauce_recipe(Meal.maple_bar, 2, Season.summer, 14, {ArtisanGood.maple_syrup: 1, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}) miners_treat = skill_recipe(Meal.miners_treat, Skill.mining, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.cow_milk: 1}) +moss_soup = skill_recipe(Meal.moss_soup, Skill.foraging, 3, {Material.moss: 20}) omelet = queen_of_sauce_recipe(Meal.omelet, 1, Season.spring, 28, {AnimalProduct.chicken_egg: 1, AnimalProduct.cow_milk: 1}) pale_broth = friendship_recipe(Meal.pale_broth, NPC.marnie, 3, {WaterItem.white_algae: 2}) pancakes = queen_of_sauce_recipe(Meal.pancakes, 1, Season.summer, 14, {Ingredient.wheat_flour: 1, AnimalProduct.chicken_egg: 1}) @@ -160,13 +162,14 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, spaghetti = friendship_recipe(Meal.spaghetti, NPC.lewis, 3, {Vegetable.tomato: 1, Ingredient.wheat_flour: 1}) spicy_eel = friendship_recipe(Meal.spicy_eel, NPC.george, 7, {Fish.eel: 1, Fruit.hot_pepper: 1}) squid_ink_ravioli = skill_recipe(Meal.squid_ink_ravioli, Skill.combat, 9, {AnimalProduct.squid_ink: 1, Ingredient.wheat_flour: 1, Vegetable.tomato: 1}) -stir_fry_ingredients = {Forageable.cave_carrot: 1, Forageable.common_mushroom: 1, Vegetable.kale: 1, Ingredient.sugar: 1} +stir_fry_ingredients = {Forageable.cave_carrot: 1, Mushroom.common: 1, Vegetable.kale: 1, Ingredient.sugar: 1} stir_fry_qos = queen_of_sauce_recipe(Meal.stir_fry, 1, Season.spring, 7, stir_fry_ingredients) strange_bun = friendship_recipe(Meal.strange_bun, NPC.shane, 7, {Ingredient.wheat_flour: 1, Fish.periwinkle: 1, ArtisanGood.void_mayonnaise: 1}) stuffing = friendship_recipe(Meal.stuffing, NPC.pam, 7, {Meal.bread: 1, Fruit.cranberries: 1, Forageable.hazelnut: 1}) super_meal = friendship_recipe(Meal.super_meal, NPC.kent, 7, {Vegetable.bok_choy: 1, Fruit.cranberries: 1, Vegetable.artichoke: 1}) -survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 2, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1}) -tom_kha_soup = friendship_recipe(Meal.tom_kha_soup, NPC.sandy, 7, {Forageable.coconut: 1, Fish.shrimp: 1, Forageable.common_mushroom: 1}) + +survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 8, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1}) +tom_kha_soup = friendship_recipe(Meal.tom_kha_soup, NPC.sandy, 7, {Forageable.coconut: 1, Fish.shrimp: 1, Mushroom.common: 1}) tortilla_ingredients = {Vegetable.corn: 1} tortilla_qos = queen_of_sauce_recipe(Meal.tortilla, 1, Season.fall, 7, tortilla_ingredients) tortilla_saloon = shop_recipe(Meal.tortilla, Region.saloon, 100, tortilla_ingredients) @@ -175,7 +178,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, trout_soup = queen_of_sauce_recipe(Meal.trout_soup, 1, Season.fall, 14, {Fish.rainbow_trout: 1, WaterItem.green_algae: 1}) vegetable_medley = friendship_recipe(Meal.vegetable_medley, NPC.caroline, 7, {Vegetable.tomato: 1, Vegetable.beet: 1}) -magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Forageable.purple_mushroom: 1}, ModNames.magic) +magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Mushroom.purple: 1}, ModNames.magic) baked_berry_oatmeal = shop_recipe(SVEMeal.baked_berry_oatmeal, SVERegion.bear_shop, 0, {Forageable.salmonberry: 15, Forageable.blackberry: 15, Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve) @@ -188,18 +191,18 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, glazed_butterfish = friendship_and_shop_recipe(SVEMeal.glazed_butterfish, NPC.gus, 10, Region.saloon, 4000, {SVEFish.butterfish: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}, ModNames.sve) mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fruit.strawberry: 6, SVEFruit.salal_berry: 6, Forageable.blackberry: 6, - SVEForage.bearberrys: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}, + SVEForage.bearberry: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}, ModNames.sve) mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve) -seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEFish.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) +seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000, {SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve) void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, {Fish.void_salmon: 1, ArtisanGood.void_mayonnaise: 1, WaterItem.seaweed: 3}, ModNames.sve) -mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.goblin, 2, {Forageable.chanterelle: 1, Forageable.common_mushroom: 1, - Forageable.red_mushroom: 1, Material.wood: 1}, ModNames.distant_lands) +mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.goblin, 2, {Mushroom.chanterelle: 1, Mushroom.common: 1, + Mushroom.red: 1, Material.wood: 1}, ModNames.distant_lands) void_mint_tea = friendship_recipe(DistantLandsMeal.void_mint_tea, ModNPC.goblin, 4, {DistantLandsCrop.void_mint: 1}, ModNames.distant_lands) crayfish_soup = friendship_recipe(DistantLandsMeal.crayfish_soup, ModNPC.goblin, 6, {Forageable.cave_carrot: 1, Fish.crayfish: 1, DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, ModNames.distant_lands) @@ -208,6 +211,11 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, special_pumpkin_soup = friendship_recipe(BoardingHouseMeal.special_pumpkin_soup, ModNPC.joel, 6, {Vegetable.pumpkin: 2, AnimalProduct.large_goat_milk: 1, Vegetable.garlic: 1}, ModNames.boarding_house) +diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology) +rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, ModNames.archaeology) +ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, ModNames.archaeology) +grilled_cheese = skill_recipe(TrashyMeal.grilled_cheese, ModSkill.binning, 1, {Meal.bread: 1, ArtisanGood.cheese: 1}, ModNames.binning_skill) +fish_casserole = skill_recipe(TrashyMeal.fish_casserole, ModSkill.binning, 8, {Fish.any: 1, AnimalProduct.milk: 1, Vegetable.carrot: 1}, ModNames.binning_skill) all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes} \ No newline at end of file diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py index 8dd622e926e7..ead4d62f1650 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,21 @@ def __repr__(self): return f"SkillSource at level {self.level} {self.skill}" +class SkillCraftsanitySource(SkillSource): + def __repr__(self): + return f"SkillCraftsanitySource at level {self.level} {self.skill}" + + +class MasterySource(RecipeSource): + skill: str + + def __init__(self, skill: str): + self.skill = skill + + def __repr__(self): + return f"MasterySource at level {self.level} {self.skill}" + + class ShopSource(RecipeSource): region: str price: int diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py new file mode 100644 index 000000000000..b2416d8d0b72 --- /dev/null +++ b/worlds/stardew_valley/data/requirement.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass + +from .game_item import Requirement +from ..strings.tool_names import ToolMaterial + + +@dataclass(frozen=True) +class BookRequirement(Requirement): + book: str + + +@dataclass(frozen=True) +class ToolRequirement(Requirement): + tool: str + tier: str = ToolMaterial.basic + + +@dataclass(frozen=True) +class SkillRequirement(Requirement): + skill: str + level: int + + +@dataclass(frozen=True) +class SeasonRequirement(Requirement): + season: str + + +@dataclass(frozen=True) +class YearRequirement(Requirement): + year: int + + +@dataclass(frozen=True) +class CombatRequirement(Requirement): + level: str + + +@dataclass(frozen=True) +class QuestRequirement(Requirement): + quest: str + + +@dataclass(frozen=True) +class RelationshipRequirement(Requirement): + npc: str + hearts: int + + +@dataclass(frozen=True) +class FishingRequirement(Requirement): + region: str + + +@dataclass(frozen=True) +class WalnutRequirement(Requirement): + amount: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py new file mode 100644 index 000000000000..cc9506023f19 --- /dev/null +++ b/worlds/stardew_valley/data/shop.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass +from typing import Tuple, Optional + +from .game_item import ItemSource +from ..strings.season_names import Season + +ItemPrice = Tuple[int, str] + + +@dataclass(frozen=True, kw_only=True) +class ShopSource(ItemSource): + shop_region: str + money_price: Optional[int] = None + items_price: Optional[Tuple[ItemPrice, ...]] = None + seasons: Tuple[str, ...] = Season.all + + def __post_init__(self): + assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." + assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." + + +@dataclass(frozen=True, kw_only=True) +class MysteryBoxSource(ItemSource): + amount: int + + +@dataclass(frozen=True, kw_only=True) +class ArtifactTroveSource(ItemSource): + amount: int + + +@dataclass(frozen=True, kw_only=True) +class PrizeMachineSource(ItemSource): + amount: int + + +@dataclass(frozen=True, kw_only=True) +class FishingTreasureChestSource(ItemSource): + amount: int diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py new file mode 100644 index 000000000000..df4ff9feed6d --- /dev/null +++ b/worlds/stardew_valley/data/skill.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass, field +from functools import cached_property +from typing import Iterable, Tuple + + +@dataclass(frozen=True) +class Skill: + name: str + has_mastery: bool = field(kw_only=True) + + @cached_property + def mastery_name(self) -> str: + return f"{self.name} Mastery" + + @cached_property + def level_name(self) -> str: + return f"{self.name} Level" + + @cached_property + def level_names_by_level(self) -> Iterable[Tuple[int, str]]: + return tuple((level, f"Level {level} {self.name}") for level in range(1, 11)) diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py index 718bce743b1c..70fb110ffbae 100644 --- a/worlds/stardew_valley/data/villagers_data.py +++ b/worlds/stardew_valley/data/villagers_data.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import List, Tuple, Optional, Dict, Callable, Set +from typing import Tuple, Optional from ..mods.mod_data import ModNames from ..strings.food_names import Beverage from ..strings.generic_names import Generic -from ..strings.region_names import Region, SVERegion, AlectoRegion, BoardingHouseRegion, LaceyRegion +from ..strings.region_names import Region, SVERegion, AlectoRegion, BoardingHouseRegion, LaceyRegion, LogicRegion from ..strings.season_names import Season from ..strings.villager_names import NPC, ModNPC @@ -36,7 +36,7 @@ def __repr__(self): alex_house = (Region.alex_house,) elliott_house = (Region.elliott_house,) ranch = (Region.ranch,) -mines_dwarf_shop = (Region.mines_dwarf_shop,) +mines_dwarf_shop = (LogicRegion.mines_dwarf_shop,) desert = (Region.desert,) oasis = (Region.oasis,) sewers = (Region.sewer,) @@ -355,28 +355,10 @@ def __repr__(self): susan_loves = pancakes + chocolate_cake + pink_cake + ice_cream + cookie + pumpkin_pie + rhubarb_pie + \ blueberry_tart + blackberry_cobbler + cranberry_candy + red_plate -all_villagers: List[Villager] = [] -villager_modifications_by_mod: Dict[str, Dict[str, Callable[[str, Villager], Villager]]] = {} - def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: str, gifts: Tuple[str, ...], available: bool, mod_name: Optional[str] = None) -> Villager: - npc = Villager(name, bachelor, locations, birthday, gifts, available, mod_name) - all_villagers.append(npc) - return npc - - -def adapt_wizard_to_sve(mod_name: str, npc: Villager): - if npc.mod_name: - mod_name = npc.mod_name - # The wizard leaves his tower on sunday, for like 1 hour... Good enough to meet him! - return Villager(npc.name, True, npc.locations + forest, npc.birthday, npc.gifts, npc.available, mod_name) - - -def register_villager_modification(mod_name: str, npc: Villager, modification_function): - if mod_name not in villager_modifications_by_mod: - villager_modifications_by_mod[mod_name] = {} - villager_modifications_by_mod[mod_name][npc.name] = modification_function + return Villager(name, bachelor, locations, birthday, gifts, available, mod_name) josh = villager(NPC.alex, True, town + alex_house, Season.summer, universal_loves + complete_breakfast + salmon_dinner, True) @@ -385,18 +367,18 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu sam = villager(NPC.sam, True, town, Season.summer, universal_loves + sam_loves, True) sebastian = villager(NPC.sebastian, True, carpenter, Season.winter, universal_loves + sebastian_loves, True) shane = villager(NPC.shane, True, ranch, Season.spring, universal_loves + shane_loves, True) -best_girl = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True) +abigail = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True) emily = villager(NPC.emily, True, town, Season.spring, universal_loves + emily_loves, True) -hoe = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True) +haley = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True) leah = villager(NPC.leah, True, forest, Season.winter, universal_loves + leah_loves, True) -nerd = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True) +maru = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True) penny = villager(NPC.penny, True, town, Season.fall, universal_loves_no_rabbit_foot + penny_loves, True) caroline = villager(NPC.caroline, False, town, Season.winter, universal_loves + caroline_loves, True) clint = villager(NPC.clint, False, town, Season.winter, universal_loves + clint_loves, True) demetrius = villager(NPC.demetrius, False, carpenter, Season.summer, universal_loves + demetrius_loves, True) dwarf = villager(NPC.dwarf, False, mines_dwarf_shop, Season.summer, universal_loves + dwarf_loves, False) -gilf = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True) -boomer = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True) +evelyn = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True) +george = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True) gus = villager(NPC.gus, False, town, Season.summer, universal_loves + gus_loves, True) jas = villager(NPC.jas, False, ranch, Season.summer, universal_loves + jas_loves, True) jodi = villager(NPC.jodi, False, town, Season.fall, universal_loves + jodi_loves, True) @@ -408,7 +390,7 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu marnie = villager(NPC.marnie, False, ranch, Season.fall, universal_loves + marnie_loves, True) pam = villager(NPC.pam, False, town, Season.spring, universal_loves + pam_loves, True) pierre = villager(NPC.pierre, False, town, Season.spring, universal_loves + pierre_loves, True) -milf = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True) +robin = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True) sandy = villager(NPC.sandy, False, oasis, Season.fall, universal_loves + sandy_loves, False) vincent = villager(NPC.vincent, False, town, Season.spring, universal_loves + vincent_loves, True) willy = villager(NPC.willy, False, beach, Season.summer, universal_loves + willy_loves, True) @@ -443,54 +425,10 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu victor = villager(ModNPC.victor, True, town, Season.summer, universal_loves + victor_loves, True, ModNames.sve) andy = villager(ModNPC.andy, False, forest, Season.spring, universal_loves + andy_loves, True, ModNames.sve) apples = villager(ModNPC.apples, False, aurora + junimo, Generic.any, starfruit, False, ModNames.sve) -gunther = villager(ModNPC.gunther, False, museum, Season.winter, universal_loves + gunther_loves, True, ModNames.jasper_sve) +gunther = villager(ModNPC.gunther, False, museum, Season.winter, universal_loves + gunther_loves, True, ModNames.sve) martin = villager(ModNPC.martin, False, town + jojamart, Season.summer, universal_loves + martin_loves, True, ModNames.sve) -marlon = villager(ModNPC.marlon, False, adventurer, Season.winter, universal_loves + marlon_loves, False, ModNames.jasper_sve) +marlon = villager(ModNPC.marlon, False, adventurer, Season.winter, universal_loves + marlon_loves, False, ModNames.sve) morgan = villager(ModNPC.morgan, False, forest, Season.fall, universal_loves_no_rabbit_foot + morgan_loves, False, ModNames.sve) scarlett = villager(ModNPC.scarlett, False, bluemoon, Season.summer, universal_loves + scarlett_loves, False, ModNames.sve) susan = villager(ModNPC.susan, False, railroad, Season.fall, universal_loves + susan_loves, False, ModNames.sve) morris = villager(ModNPC.morris, False, jojamart, Season.spring, universal_loves + morris_loves, True, ModNames.sve) - -# Modified villagers; not included in all villagers - -register_villager_modification(ModNames.sve, wizard, adapt_wizard_to_sve) - -all_villagers_by_name: Dict[str, Villager] = {villager.name: villager for villager in all_villagers} -all_villagers_by_mod: Dict[str, List[Villager]] = {} -all_villagers_by_mod_by_name: Dict[str, Dict[str, Villager]] = {} - -for npc in all_villagers: - mod = npc.mod_name - name = npc.name - if mod in all_villagers_by_mod: - all_villagers_by_mod[mod].append(npc) - all_villagers_by_mod_by_name[mod][name] = npc - else: - all_villagers_by_mod[mod] = [npc] - all_villagers_by_mod_by_name[mod] = {} - all_villagers_by_mod_by_name[mod][name] = npc - - -def villager_included_for_any_mod(npc: Villager, mods: Set[str]): - if not npc.mod_name: - return True - for mod in npc.mod_name.split(","): - if mod in mods: - return True - return False - - -def get_villagers_for_mods(mods: Set[str]) -> List[Villager]: - villagers_for_current_mods = [] - for npc in all_villagers: - if not villager_included_for_any_mod(npc, mods): - continue - modified_npc = npc - for active_mod in mods: - if (active_mod not in villager_modifications_by_mod or - npc.name not in villager_modifications_by_mod[active_mod]): - continue - modification = villager_modifications_by_mod[active_mod][npc.name] - modified_npc = modification(active_mod, modified_npc) - villagers_for_current_mods.append(modified_npc) - return villagers_for_current_mods diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index c29ae859e095..62755dad798d 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -11,59 +11,62 @@ A vast number of objectives in Stardew Valley can be shuffled around the multiwo player can customize their experience in their YAML file. For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining -number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that +number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player. ## What is the goal of Stardew Valley? The player can choose from a number of goals, using their YAML options. + - Complete the [Community Center](https://stardewvalleywiki.com/Bundles) - Succeed [Grandpa's Evaluation](https://stardewvalleywiki.com/Grandpa) with 4 lit candles - Reach the bottom of the [Pelican Town Mineshaft](https://stardewvalleywiki.com/The_Mines) -- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on -floor 100 of the Skull Cavern +- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on + floor 100 of the Skull Cavern - Become a [Master Angler](https://stardewvalleywiki.com/Fish), which requires catching every fish in your slot -- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and -minerals to the museum +- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and + minerals to the museum - Get the achievement [Full House](https://stardewvalleywiki.com/Children), which requires getting married and having two kids -- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires -finding all 130 golden walnuts on ginger island -- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by -completing all the monster slayer goals at the Adventure Guild +- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires + finding all 130 golden walnuts on ginger island +- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by + completing all the monster slayer goals at the Adventure Guild - Complete a [Full Shipment](https://stardewvalleywiki.com/Shipping#Collection) by shipping every item in your slot - Become a [Gourmet Chef](https://stardewvalleywiki.com/Cooking) by cooking every recipe in your slot - Become a [Craft Master](https://stardewvalleywiki.com/Crafting) by crafting every item -- Earn the title of [Legend](https://stardewvalleywiki.com/Gold) by earning 10 000 000g -- Solve the [Mystery of the Stardrops](https://stardewvalleywiki.com/Stardrop) by finding every stardrop +- Earn the title of [Legend](https://stardewvalleywiki.com/Gold) by earning 10 000 000g +- Solve the [Mystery of the Stardrops](https://stardewvalleywiki.com/Stardrop) by finding every stardrop - Finish 100% of your randomizer slot with Allsanity: Complete every check in your slot - Achieve [Perfection](https://stardewvalleywiki.com/Perfection) in your save file -The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt -to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" +The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt +to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" to "Exclude Legendaries", and pick the Master Angler goal, you will not need to catch the legendaries to complete the goal. ## What are location checks in Stardew Valley? Location checks in Stardew Valley always include: + - [Community Center Bundles](https://stardewvalleywiki.com/Bundles) - [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards) - [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart) -- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), -[Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), -[Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc +- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), + [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), + [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: + - [Tools and Fishing Rod Upgrades](https://stardewvalleywiki.com/Tools) - [Carpenter Buildings](https://stardewvalleywiki.com/Carpenter%27s_Shop#Farm_Buildings) - [Backpack Upgrades](https://stardewvalleywiki.com/Tools#Other_Tools) - [Mine Elevator Levels](https://stardewvalleywiki.com/The_Mines#Staircases) -- [Skill Levels](https://stardewvalleywiki.com/Skills) +- [Skill Levels](https://stardewvalleywiki.com/Skills) and [Masteries](https://stardewvalleywiki.com/Mastery_Cave#Masteries) - Arcade Machines - [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests) - [Help Wanted Quests](https://stardewvalleywiki.com/Quests#Help_Wanted_Quests) - Participating in [Festivals](https://stardewvalleywiki.com/Festivals) -- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from -[Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) +- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from + [Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) - [Cropsanity](https://stardewvalleywiki.com/Crops): Growing and Harvesting individual crop types - [Fishsanity](https://stardewvalleywiki.com/Fish): Catching individual fish - [Museumsanity](https://stardewvalleywiki.com/Museum): Donating individual items, or reaching milestones for museum donations @@ -73,6 +76,8 @@ There also are a number of location checks that are optional, and individual pla - [Chefsanity](https://stardewvalleywiki.com/Cooking#Recipes): Learning cooking recipes - [Craftsanity](https://stardewvalleywiki.com/Crafting): Crafting individual items - [Shipsanity](https://stardewvalleywiki.com/Shipping): Shipping individual items +- [Booksanity](https://stardewvalleywiki.com/Books): Reading individual books +- [Walnutsanity](https://stardewvalleywiki.com/Golden_Walnut): Collecting Walnuts on Ginger Island ## Which items can be in another player's world? @@ -80,68 +85,73 @@ Every normal reward from the above locations can be in another player's world. For the locations which do not include a normal reward, Resource Packs and traps are instead added to the pool. Traps are optional. A player can enable some options that will add some items to the pool that are relevant to progression + - Seasons Randomizer: - - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. - - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received. + - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. + - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only + choose from the seasons they have received. - Cropsanity: - - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each seed and harvesting the resulting crop sends a location check - - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually. + - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each + seed and harvesting the resulting crop sends a location check + - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount + packs, not individually. - Museumsanity: - - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience. - - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items. + - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for + convenience. + - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the + player receives "Traveling Merchant Metal Detector" items. - TV Channels - Babies - Only if Friendsanity is enabled There are a few extra vanilla items, which are added to the pool for convenience, but do not have a matching location. These include + - [Wizard Buildings](https://stardewvalleywiki.com/Wizard%27s_Tower#Buildings) - [Return Scepter](https://stardewvalleywiki.com/Return_Scepter) - [Qi Walnut Room QoL items](https://stardewvalleywiki.com/Qi%27s_Walnut_Room#Stock) And lastly, some Archipelago-exclusive items exist in the pool, which are designed around game balance and QoL. These include: + - Arcade Machine buffs (Only if the arcade machines are randomized) - - Journey of the Prairie King has drop rate increases, extra lives, and equipment - - Junimo Kart has extra lives. + - Journey of the Prairie King has drop rate increases, extra lives, and equipment + - Junimo Kart has extra lives. - Permanent Movement Speed Bonuses (customizable) -- Permanent Luck Bonuses (customizable) -- Traveling Merchant buffs +- Various Permanent Player Buffs (customizable) +- Traveling Merchant modifiers ## When the player receives an item, what happens? -Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received -while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where +Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received +while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where it was found. If an item is received while offline, it will be in the mailbox as soon as the player logs in. -Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter +Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter only serves to tell the player about it. -In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the +In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the building that they have received, so they can choose its position. This construction will be completely free. ## Mods Some Stardew Valley mods unrelated to Archipelago are officially "supported". -This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated -with the assumption that you will install and play with these mods. The multiworld will contain related items and locations +This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated +with the assumption that you will install and play with these mods. The multiworld will contain related items and locations for these mods, the specifics will vary from mod to mod -[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) +[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) List of supported mods: - General - [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753) - - [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571) - [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963) - [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845) - [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401) - [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109) - Skills - - [Magic](https://www.nexusmods.com/stardewvalley/mods/2007) - [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521) - [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142) - - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793) - - [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522) + - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/22199) - [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073) - NPCs - [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427) @@ -149,20 +159,15 @@ List of supported mods: - [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606) - [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599) - [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697) - - [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871) - [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222) - - ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462) - - [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732) - - [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510) - - [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811) - [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671) - -Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found + +Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found [here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases) ## Multiplayer You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. -You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game -Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. +You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew , or a player in another game that supports gifting, using +in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 74caf9b7daba..801bf345e916 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -2,21 +2,17 @@ ## Required Software -- Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) - - You need version 1.5.6. It is available in a public beta branch on Steam ![image](https://i.imgur.com/uKAUmF0.png). - - If your Stardew is not on Steam, you are responsible for finding a way to downgrade it. - - This measure is temporary. We are working hard to bring the mod to Stardew 1.6 as soon as possible. -- SMAPI 3.x.x ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) - - Same as Stardew Valley itself, SMAPI needs a slightly older version to be compatible with Stardew Valley 1.5.6 ![image](https://i.imgur.com/kzgObHy.png) -- [StardewArchipelago Mod Release 5.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - - It is important to use a mod release of version 5.x.x to play seeds that have been generated here. Later releases +- Stardew Valley 1.6 on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) +- SMAPI ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) +- [StardewArchipelago Mod Release 6.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) + - It is important to use a mod release of version 6.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. ## Optional Software - Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) * (Only for the TextClient) - Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley) - * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) + * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) that you can add to your yaml to include them with the Archipelago randomization * It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so. @@ -38,7 +34,7 @@ You can customize your options by visiting the [Stardew Valley Player Options Pa ### Installing the mod -- Install [SMAPI version 3.x.x](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page +- Install [SMAPI](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page - Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder - *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%` @@ -93,7 +89,7 @@ Stardew-exclusive commands. ### Playing with supported mods -See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) +See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) ### Multiplayer diff --git a/worlds/stardew_valley/early_items.py b/worlds/stardew_valley/early_items.py index 78170f29fee7..81e28956b3cf 100644 --- a/worlds/stardew_valley/early_items.py +++ b/worlds/stardew_valley/early_items.py @@ -1,51 +1,72 @@ from random import Random -from .options import BuildingProgression, StardewValleyOptions, BackpackProgression, ExcludeGingerIsland, SeasonRandomization, SpecialOrderLocations, \ - Monstersanity, ToolProgression, SkillProgression, Cooksanity, Chefsanity +from . import options as stardew_options +from .content import StardewContent +from .strings.ap_names.ap_weapon_names import APWeapon +from .strings.ap_names.transport_names import Transportation +from .strings.building_names import Building +from .strings.region_names import Region +from .strings.season_names import Season +from .strings.skill_names import Skill +from .strings.tv_channel_names import Channel +from .strings.wallet_item_names import Wallet early_candidate_rate = 4 -always_early_candidates = ["Greenhouse", "Desert Obelisk", "Rusty Key"] -seasons = ["Spring", "Summer", "Fall", "Winter"] +always_early_candidates = [Region.greenhouse, Transportation.desert_obelisk, Wallet.rusty_key] +seasons = [Season.spring, Season.summer, Season.fall, Season.winter] -def setup_early_items(multiworld, options: StardewValleyOptions, player: int, random: Random): +def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, content: StardewContent, player: int, random: Random): early_forced = [] early_candidates = [] early_candidates.extend(always_early_candidates) add_seasonal_candidates(early_candidates, options) - if options.building_progression & BuildingProgression.option_progressive: - early_forced.append("Shipping Bin") - early_candidates.append("Progressive Coop") + if options.building_progression & stardew_options.BuildingProgression.option_progressive: + early_forced.append(Building.shipping_bin) + if options.farm_type != stardew_options.FarmType.option_meadowlands: + early_candidates.append("Progressive Coop") early_candidates.append("Progressive Barn") - if options.backpack_progression == BackpackProgression.option_early_progressive: + if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive: early_forced.append("Progressive Backpack") - if options.tool_progression & ToolProgression.option_progressive: - early_forced.append("Progressive Fishing Rod") + if options.tool_progression & stardew_options.ToolProgression.option_progressive: + if content.features.fishsanity.is_enabled: + early_candidates.append("Progressive Fishing Rod") early_forced.append("Progressive Pickaxe") - if options.skill_progression == SkillProgression.option_progressive: - early_forced.append("Fishing Level") + fishing = content.skills.get(Skill.fishing) + if fishing is not None and content.features.skill_progression.is_progressive: + early_forced.append(fishing.level_name) if options.quest_locations >= 0: - early_candidates.append("Magnifying Glass") + early_candidates.append(Wallet.magnifying_glass) - if options.special_order_locations != SpecialOrderLocations.option_disabled: + if options.special_order_locations & stardew_options.SpecialOrderLocations.option_board: early_candidates.append("Special Order Board") - if options.cooksanity != Cooksanity.option_none | options.chefsanity & Chefsanity.option_queen_of_sauce: - early_candidates.append("The Queen of Sauce") + if options.cooksanity != stardew_options.Cooksanity.option_none or options.chefsanity & stardew_options.Chefsanity.option_queen_of_sauce: + early_candidates.append(Channel.queen_of_sauce) - if options.monstersanity == Monstersanity.option_none: - early_candidates.append("Progressive Weapon") + if options.craftsanity != stardew_options.Craftsanity.option_none: + early_candidates.append("Furnace Recipe") + + if options.monstersanity == stardew_options.Monstersanity.option_none: + early_candidates.append(APWeapon.weapon) else: - early_candidates.append("Progressive Sword") + early_candidates.append(APWeapon.sword) + + if options.exclude_ginger_island == stardew_options.ExcludeGingerIsland.option_false: + early_candidates.append(Transportation.island_obelisk) + + if options.walnutsanity.value: + early_candidates.append("Island North Turtle") + early_candidates.append("Island West Turtle") - if options.exclude_ginger_island == ExcludeGingerIsland.option_false: - early_candidates.append("Island Obelisk") + if options.museumsanity != stardew_options.Museumsanity.option_none or options.shipsanity >= stardew_options.Shipsanity.option_full_shipment: + early_candidates.append(Wallet.metal_detector) early_forced.extend(random.sample(early_candidates, len(early_candidates) // early_candidate_rate)) @@ -56,10 +77,10 @@ def setup_early_items(multiworld, options: StardewValleyOptions, player: int, ra def add_seasonal_candidates(early_candidates, options): - if options.season_randomization == SeasonRandomization.option_progressive: - early_candidates.extend(["Progressive Season"] * 3) + if options.season_randomization == stardew_options.SeasonRandomization.option_progressive: + early_candidates.extend([Season.progressive] * 3) return - if options.season_randomization == SeasonRandomization.option_disabled: + if options.season_randomization == stardew_options.SeasonRandomization.option_disabled: return early_candidates.extend(seasons) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index d0cb09bd9953..6ac827f869cc 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -2,24 +2,27 @@ import enum import logging from dataclasses import dataclass, field +from functools import reduce from pathlib import Path from random import Random from typing import Dict, List, Protocol, Union, Set, Optional from BaseClasses import Item, ItemClassification from . import data -from .data.villagers_data import get_villagers_for_mods +from .content.feature import friendsanity +from .content.game_content import StardewContent +from .data.game_item import ItemTag +from .logic.logic_event import all_events from .mods.mod_data import ModNames -from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Cropsanity, \ - Friendsanity, Museumsanity, \ - Fishsanity, BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ - Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity +from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \ + BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ + Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs +from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.buff_names import Buff from .strings.ap_names.community_upgrade_names import CommunityUpgrade -from .strings.ap_names.event_names import Event from .strings.ap_names.mods.mod_items import SVEQuestItem -from .strings.villager_names import NPC, ModNPC +from .strings.currency_names import Currency from .strings.wallet_item_names import Wallet ITEM_CODE_OFFSET = 717000 @@ -44,6 +47,7 @@ class Group(enum.Enum): WEAPON_SLINGSHOT = enum.auto() PROGRESSIVE_TOOLS = enum.auto() SKILL_LEVEL_UP = enum.auto() + SKILL_MASTERY = enum.auto() BUILDING = enum.auto() WIZARD_BUILDING = enum.auto() ARCADE_MACHINE_BUFFS = enum.auto() @@ -62,6 +66,7 @@ class Group(enum.Enum): FESTIVAL = enum.auto() RARECROW = enum.auto() TRAP = enum.auto() + BONUS = enum.auto() MAXIMUM_ONE = enum.auto() EXACTLY_TWO = enum.auto() DEPRECATED = enum.auto() @@ -80,6 +85,9 @@ class Group(enum.Enum): CHEFSANITY_FRIENDSHIP = enum.auto() CHEFSANITY_SKILL = enum.auto() CRAFTSANITY = enum.auto() + BOOK_POWER = enum.auto() + LOST_BOOK = enum.auto() + PLAYER_BUFF = enum.auto() # Mods MAGIC_SPELL = enum.auto() MOD_WARP = enum.auto() @@ -117,17 +125,14 @@ def __call__(self, item: Item): def load_item_csv(): - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files # noqa + from importlib.resources import files items = [] with files(data).joinpath("items.csv").open() as file: item_reader = csv.DictReader(file) for item in item_reader: id = int(item["id"]) if item["id"] else None - classification = ItemClassification[item["classification"]] + classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")}) groups = {Group[group] for group in item["groups"].split(",") if group} mod_name = str(item["mod_name"]) if item["mod_name"] else None items.append(ItemData(id, item["name"], classification, mod_name, groups)) @@ -135,11 +140,8 @@ def load_item_csv(): events = [ - ItemData(None, Event.victory, ItemClassification.progression), - ItemData(None, Event.can_construct_buildings, ItemClassification.progression), - ItemData(None, Event.start_dark_talisman_quest, ItemClassification.progression), - ItemData(None, Event.can_ship_items, ItemClassification.progression), - ItemData(None, Event.can_shop_at_pierre, ItemClassification.progression), + ItemData(None, e, ItemClassification.progression) + for e in sorted(all_events) ] all_items: List[ItemData] = load_item_csv() + events @@ -167,14 +169,14 @@ def get_too_many_items_error_message(locations_count: int, items_count: int) -> return f"There should be at least as many locations [{locations_count}] as there are mandatory items [{items_count}]" -def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDeleter, locations_count: int, items_to_exclude: List[Item], - options: StardewValleyOptions, random: Random) -> List[Item]: +def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item], + options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: items = [] - unique_items = create_unique_items(item_factory, options, random) + unique_items = create_unique_items(item_factory, options, content, random) - remove_items(item_deleter, items_to_exclude, unique_items) + remove_items(items_to_exclude, unique_items) - remove_items_if_no_room_for_them(item_deleter, unique_items, locations_count, random) + remove_items_if_no_room_for_them(unique_items, locations_count, random) items += unique_items logger.debug(f"Created {len(unique_items)} unique items") @@ -190,14 +192,13 @@ def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDele return items -def remove_items(item_deleter: StardewItemDeleter, items_to_remove, items): +def remove_items(items_to_remove, items): for item in items_to_remove: if item in items: items.remove(item) - item_deleter(item) -def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_items: List[Item], locations_count: int, random: Random): +def remove_items_if_no_room_for_them(unique_items: List[Item], locations_count: int, random: Random): if len(unique_items) <= locations_count: return @@ -210,22 +211,23 @@ def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_it logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random filler items") assert len(removable_items) >= number_of_items_to_remove, get_too_many_items_error_message(locations_count, len(unique_items)) items_to_remove = random.sample(removable_items, number_of_items_to_remove) - remove_items(item_deleter, items_to_remove, unique_items) + remove_items(items_to_remove, unique_items) -def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random) -> List[Item]: +def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: items = [] items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) items.append(item_factory(CommunityUpgrade.movie_theater)) # It is a community reward, but we need two of them + create_raccoons(item_factory, options, items) items.append(item_factory(Wallet.metal_detector)) # Always offer at least one metal detector create_backpack_items(item_factory, options, items) create_weapons(item_factory, options, items) items.append(item_factory("Skull Key")) create_elevators(item_factory, options, items) - create_tools(item_factory, options, items) - create_skills(item_factory, options, items) + create_tools(item_factory, options, content, items) + create_skills(item_factory, content, items) create_wizard_buildings(item_factory, options, items) create_carpenter_buildings(item_factory, options, items) items.append(item_factory("Railroad Boulder Removed")) @@ -233,25 +235,30 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley items.append(item_factory(CommunityUpgrade.mushroom_boxes)) items.append(item_factory("Beach Bridge")) create_tv_channels(item_factory, options, items) - create_special_quest_rewards(item_factory, options, items) - create_stardrops(item_factory, options, items) + create_quest_rewards(item_factory, options, items) + create_stardrops(item_factory, options, content, items) create_museum_items(item_factory, options, items) create_arcade_machine_items(item_factory, options, items) - create_player_buffs(item_factory, options, items) + create_movement_buffs(item_factory, options, items) create_traveling_merchant_items(item_factory, items) items.append(item_factory("Return Scepter")) create_seasons(item_factory, options, items) - create_seeds(item_factory, options, items) - create_friendsanity_items(item_factory, options, items, random) + create_seeds(item_factory, content, items) + create_friendsanity_items(item_factory, options, content, items, random) create_festival_rewards(item_factory, options, items) create_special_order_board_rewards(item_factory, options, items) create_special_order_qi_rewards(item_factory, options, items) + create_walnuts(item_factory, options, items) create_walnut_purchase_rewards(item_factory, options, items) create_crafting_recipes(item_factory, options, items) create_cooking_recipes(item_factory, options, items) create_shipsanity_items(item_factory, options, items) + create_booksanity_items(item_factory, content, items) create_goal_items(item_factory, options, items) items.append(item_factory("Golden Egg")) + items.append(item_factory(CommunityUpgrade.mr_qi_plane_ride)) + + create_sve_special_items(item_factory, options, items) create_magic_mod_spells(item_factory, options, items) create_deepwoods_pendants(item_factory, options, items) create_archaeology_items(item_factory, options, items) @@ -259,6 +266,14 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley return items +def create_raccoons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + number_progressive_raccoons = 9 + if options.quest_locations < 0: + number_progressive_raccoons = number_progressive_raccoons - 1 + + items.extend(item_factory(item) for item in [CommunityUpgrade.raccoon] * number_progressive_raccoons) + + def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if (options.backpack_progression == BackpackProgression.option_progressive or options.backpack_progression == BackpackProgression.option_early_progressive): @@ -301,7 +316,7 @@ def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOpt items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8]) -def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]): if options.tool_progression & ToolProgression.option_progressive: for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]: name = item_data.name @@ -310,15 +325,29 @@ def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions items.append(item_factory(item_data, ItemClassification.useful)) else: items.extend([item_factory(item) for item in [item_data] * 4]) - items.append(item_factory("Golden Scythe")) + if content.features.skill_progression.are_masteries_shuffled: + # Masteries add another tier to the scythe and the fishing rod + items.append(item_factory("Progressive Scythe")) + items.append(item_factory("Progressive Fishing Rod")) -def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.skill_progression == SkillProgression.option_progressive: - for item in items_by_group[Group.SKILL_LEVEL_UP]: - if item.mod_name not in options.mods and item.mod_name is not None: - continue - items.extend(item_factory(item) for item in [item.name] * 10) + # The golden scythe is always randomized + items.append(item_factory("Progressive Scythe")) + + +def create_skills(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + skill_progression = content.features.skill_progression + if not skill_progression.is_progressive: + return + + for skill in content.skills.values(): + items.extend(item_factory(skill.level_name) for _ in skill_progression.get_randomized_level_names_by_level(skill)) + + if skill_progression.is_mastery_randomized(skill): + items.append(item_factory(skill.mastery_name)) + + if skill_progression.are_masteries_shuffled: + items.append(item_factory(Wallet.mastery_of_the_five_ways)) def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -360,6 +389,13 @@ def create_carpenter_buildings(item_factory: StardewItemFactory, options: Starde items.append(item_factory("Tractor Garage")) +def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + create_special_quest_rewards(item_factory, options, items) + create_help_wanted_quest_rewards(item_factory, options, items) + + create_quest_rewards_sve(item_factory, options, items) + + def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if options.quest_locations < 0: return @@ -371,23 +407,31 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, options: Star else: items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful)) # Not necessary outside of SVE items.append(item_factory(Wallet.iridium_snake_milk)) - items.append(item_factory("Fairy Dust Recipe")) items.append(item_factory("Dark Talisman")) - create_special_quest_rewards_sve(item_factory, options, items) - create_distant_lands_quest_rewards(item_factory, options, items) - create_boarding_house_quest_rewards(item_factory, options, items) + if options.exclude_ginger_island == ExcludeGingerIsland.option_false: + items.append(item_factory("Fairy Dust Recipe")) -def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.quest_locations <= 0: + return + + number_help_wanted = options.quest_locations.value + quest_per_prize_ticket = 3 + number_prize_tickets = number_help_wanted // quest_per_prize_ticket + items.extend(item_factory(item) for item in [Currency.prize_ticket] * number_prize_tickets) + + +def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]): stardrops_classification = get_stardrop_classification(options) items.append(item_factory("Stardrop", stardrops_classification)) # The Mines level 100 items.append(item_factory("Stardrop", stardrops_classification)) # Old Master Cannoli items.append(item_factory("Stardrop", stardrops_classification)) # Krobus Stardrop - if options.fishsanity != Fishsanity.option_none: + if content.features.fishsanity.is_enabled: items.append(item_factory("Stardrop", stardrops_classification)) # Master Angler Stardrop if ModNames.deepwoods in options.mods: items.append(item_factory("Stardrop", stardrops_classification)) # Petting the Unicorn - if options.friendsanity != Friendsanity.option_none: + if content.features.friendsanity.is_enabled: items.append(item_factory("Stardrop", stardrops_classification)) # Spouse Stardrop @@ -403,39 +447,23 @@ def create_museum_items(item_factory: StardewItemFactory, options: StardewValley items.append(item_factory(Wallet.metal_detector)) -def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item], random: Random): - island_villagers = [NPC.leo, ModNPC.lance] - if options.friendsanity == Friendsanity.option_none: +def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item], random: Random): + if not content.features.friendsanity.is_enabled: return + create_babies(item_factory, items, random) - exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors - exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ - options.friendsanity == Friendsanity.option_bachelors - include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - mods = options.mods - heart_size = options.friendsanity_heart_size - for villager in get_villagers_for_mods(mods.value): - if not villager.available and exclude_locked_villagers: - continue - if not villager.bachelor and exclude_non_bachelors: - continue - if villager.name in island_villagers and exclude_ginger_island: - continue - heart_cap = 8 if villager.bachelor else 10 - if include_post_marriage_hearts and villager.bachelor: - heart_cap = 14 - classification = ItemClassification.progression - for heart in range(1, 15): - if heart > heart_cap: - break - if heart % heart_size == 0 or heart == heart_cap: - items.append(item_factory(f"{villager.name} <3", classification)) - if not exclude_non_bachelors: - need_pet = options.goal == Goal.option_grandpa_evaluation - for heart in range(1, 6): - if heart % heart_size == 0 or heart == 5: - items.append(item_factory(f"Pet <3", ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful)) + + for villager in content.villagers.values(): + item_name = friendsanity.to_item_name(villager.name) + + for _ in content.features.friendsanity.get_randomized_hearts(villager): + items.append(item_factory(item_name, ItemClassification.progression)) + + need_pet = options.goal == Goal.option_grandpa_evaluation + pet_item_classification = ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful + + for _ in content.features.friendsanity.get_pet_randomized_hearts(): + items.append(item_factory(friendsanity.pet_heart_item_name, pet_item_classification)) def create_babies(item_factory: StardewItemFactory, items: List[Item], random: Random): @@ -462,26 +490,14 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, options: Stard items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8) -def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_movement_buffs(item_factory, options: StardewValleyOptions, items: List[Item]): movement_buffs: int = options.movement_buff_number.value - luck_buffs: int = options.luck_buff_number.value - need_all_buffs = options.special_order_locations == SpecialOrderLocations.option_board_qi - need_half_buffs = options.festival_locations == FestivalLocations.option_easy - create_player_buff(item_factory, Buff.movement, movement_buffs, need_all_buffs, need_half_buffs, items) - create_player_buff(item_factory, Buff.luck, luck_buffs, True, need_half_buffs, items) - - -def create_player_buff(item_factory, buff: str, amount: int, need_all_buffs: bool, need_half_buffs: bool, items: List[Item]): - progression_buffs = amount if need_all_buffs else (amount // 2 if need_half_buffs else 0) - useful_buffs = amount - progression_buffs - items.extend(item_factory(item) for item in [buff] * progression_buffs) - items.extend(item_factory(item, ItemClassification.useful) for item in [buff] * useful_buffs) + items.extend(item_factory(item) for item in [Buff.movement] * movement_buffs) def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): items.extend([*(item_factory(item) for item in items_by_group[Group.TRAVELING_MERCHANT_DAY]), - *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6), - *(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)]) + *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6)]) def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -495,14 +511,11 @@ def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptio items.extend([item_factory(item) for item in items_by_group[Group.SEASON]]) -def create_seeds(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.cropsanity == Cropsanity.option_disabled: +def create_seeds(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + if not content.features.cropsanity.is_enabled: return - base_seed_items = [item for item in items_by_group[Group.CROPSANITY]] - filtered_seed_items = remove_excluded_items(base_seed_items, options) - seed_items = [item_factory(item) for item in filtered_seed_items] - items.extend(seed_items) + items.extend(item_factory(item_table[seed.name]) for seed in content.find_tagged_items(ItemTag.CROPSANITY_SEED)) def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -514,6 +527,35 @@ def create_festival_rewards(item_factory: StardewItemFactory, options: StardewVa items.extend([*festival_rewards, item_factory("Stardrop", get_stardrop_classification(options))]) +def create_walnuts(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + walnutsanity = options.walnutsanity + if options.exclude_ginger_island == ExcludeGingerIsland.option_true or walnutsanity == Walnutsanity.preset_none: + return + + # Give baseline walnuts just to be nice + num_single_walnuts = 0 + num_triple_walnuts = 2 + num_penta_walnuts = 1 + # https://stardewvalleywiki.com/Golden_Walnut + # Totals should be accurate, but distribution is slightly offset to make room for baseline walnuts + if WalnutsanityOptionName.puzzles in walnutsanity: # 61 + num_single_walnuts += 6 # 6 + num_triple_walnuts += 5 # 15 + num_penta_walnuts += 8 # 40 + if WalnutsanityOptionName.bushes in walnutsanity: # 25 + num_single_walnuts += 16 # 16 + num_triple_walnuts += 3 # 9 + if WalnutsanityOptionName.dig_spots in walnutsanity: # 18 + num_single_walnuts += 18 # 18 + if WalnutsanityOptionName.repeatables in walnutsanity: # 33 + num_single_walnuts += 30 # 30 + num_triple_walnuts += 1 # 3 + + items.extend([item_factory(item) for item in ["Golden Walnut"] * num_single_walnuts]) + items.extend([item_factory(item) for item in ["3 Golden Walnuts"] * num_triple_walnuts]) + items.extend([item_factory(item) for item in ["5 Golden Walnuts"] * num_penta_walnuts]) + + def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if options.exclude_ginger_island == ExcludeGingerIsland.option_true: return @@ -526,12 +568,9 @@ def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: St def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.special_order_locations == SpecialOrderLocations.option_disabled: - return - - special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]] - - items.extend([item_factory(item) for item in special_order_board_items]) + if options.special_order_locations & SpecialOrderLocations.option_board: + special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]] + items.extend([item_factory(item) for item in special_order_board_items]) def special_order_board_item_classification(item: ItemData, need_all_recipes: bool) -> ItemClassification: @@ -554,7 +593,7 @@ def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: S qi_gem_rewards.append("15 Qi Gems") qi_gem_rewards.append("15 Qi Gems") - if options.special_order_locations == SpecialOrderLocations.option_board_qi: + if options.special_order_locations & SpecialOrderLocations.value_qi: qi_gem_rewards.extend(["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems", "40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"]) @@ -607,6 +646,16 @@ def create_shipsanity_items(item_factory: StardewItemFactory, options: StardewVa items.append(item_factory(Wallet.metal_detector)) +def create_booksanity_items(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + items.extend(item_factory(item_table[booksanity.to_item_name(book.name)]) for book in content.find_tagged_items(ItemTag.BOOK_POWER)) + progressive_lost_book = item_table[booksanity.progressive_lost_book] + items.extend(item_factory(progressive_lost_book) for _ in content.features.booksanity.get_randomized_lost_books()) + + def create_goal_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): goal = options.goal if goal != Goal.option_perfection and goal != Goal.option_complete_collection: @@ -643,35 +692,29 @@ def create_deepwoods_pendants(item_factory: StardewItemFactory, options: Stardew items.extend([item_factory(item) for item in ["Pendant of Elders", "Pendant of Community", "Pendant of Depths"]]) -def create_special_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_sve_special_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if ModNames.sve not in options.mods: return items.extend([item_factory(item) for item in items_by_group[Group.MOD_WARP] if item.mod_name == ModNames.sve]) - if options.quest_locations < 0: - return - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items]) - if exclude_ginger_island: +def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if ModNames.sve not in options.mods: return - items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island]) + exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true + items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items]) + if not exclude_ginger_island: + items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items_ginger_island]) -def create_distant_lands_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.quest_locations < 0 or ModNames.distant_lands not in options.mods: - return - items.append(item_factory("Crayfish Soup Recipe")) - if options.exclude_ginger_island == ExcludeGingerIsland.option_true: + if options.quest_locations < 0: return - items.append(item_factory("Ginger Tincture Recipe")) - -def create_boarding_house_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.quest_locations < 0 or ModNames.boarding_house not in options.mods: + items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items]) + if exclude_ginger_island: return - items.append(item_factory("Special Pumpkin Soup Recipe")) + items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island]) def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random, @@ -699,18 +742,21 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options items_already_added_names = [item.name for item in items_already_added] useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL] if pack.name not in items_already_added_names] - trap_items = [pack for pack in items_by_group[Group.TRAP] - if pack.name not in items_already_added_names and - (pack.mod_name is None or pack.mod_name in options.mods)] + trap_items = [trap for trap in items_by_group[Group.TRAP] + if trap.name not in items_already_added_names and + (trap.mod_name is None or trap.mod_name in options.mods)] + player_buffs = get_allowed_player_buffs(options.enabled_filler_buffs) priority_filler_items = [] priority_filler_items.extend(useful_resource_packs) + priority_filler_items.extend(player_buffs) if include_traps: priority_filler_items.extend(trap_items) exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true all_filler_packs = remove_excluded_items(get_all_filler_items(include_traps, exclude_ginger_island), options) + all_filler_packs.extend(player_buffs) priority_filler_items = remove_excluded_items(priority_filler_items, options) number_priority_items = len(priority_filler_items) @@ -776,7 +822,7 @@ def remove_limited_amount_packs(packs): return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups] -def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): +def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]: all_filler_items = [pack for pack in items_by_group[Group.RESOURCE_PACK]] all_filler_items.extend(items_by_group[Group.TRASH]) if include_traps: @@ -785,6 +831,33 @@ def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): return all_filler_items +def get_allowed_player_buffs(buff_option: EnabledFillerBuffs) -> List[ItemData]: + allowed_buffs = [] + if BuffOptionName.luck in buff_option: + allowed_buffs.append(item_table[Buff.luck]) + if BuffOptionName.damage in buff_option: + allowed_buffs.append(item_table[Buff.damage]) + if BuffOptionName.defense in buff_option: + allowed_buffs.append(item_table[Buff.defense]) + if BuffOptionName.immunity in buff_option: + allowed_buffs.append(item_table[Buff.immunity]) + if BuffOptionName.health in buff_option: + allowed_buffs.append(item_table[Buff.health]) + if BuffOptionName.energy in buff_option: + allowed_buffs.append(item_table[Buff.energy]) + if BuffOptionName.bite in buff_option: + allowed_buffs.append(item_table[Buff.bite_rate]) + if BuffOptionName.fish_trap in buff_option: + allowed_buffs.append(item_table[Buff.fish_trap]) + if BuffOptionName.fishing_bar in buff_option: + allowed_buffs.append(item_table[Buff.fishing_bar]) + if BuffOptionName.quality in buff_option: + allowed_buffs.append(item_table[Buff.quality]) + if BuffOptionName.glow in buff_option: + allowed_buffs.append(item_table[Buff.glow]) + return allowed_buffs + + def get_stardrop_classification(options) -> ItemClassification: return ItemClassification.progression_skip_balancing if world_is_perfection(options) or world_is_stardrops(options) else ItemClassification.useful diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 103b3bd96081..b3a8db6f0341 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -6,17 +6,17 @@ from . import data from .bundles.bundle_room import BundleRoom -from .data.fish_data import special_fish, get_fish_for_mods +from .content.game_content import StardewContent +from .data.game_item import ItemTag from .data.museum_data import all_museum_items -from .data.villagers_data import get_villagers_for_mods from .mods.mod_data import ModNames -from .options import ExcludeGingerIsland, Friendsanity, ArcadeMachineLocations, SpecialOrderLocations, Cropsanity, Fishsanity, Museumsanity, FestivalLocations, \ - SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression +from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \ + FestivalLocations, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity from .strings.goal_names import Goal -from .strings.quest_names import ModQuest -from .strings.region_names import Region -from .strings.villager_names import NPC, ModNPC +from .strings.quest_names import ModQuest, Quest +from .strings.region_names import Region, LogicRegion +from .strings.villager_names import NPC LOCATION_CODE_OFFSET = 717000 @@ -32,6 +32,7 @@ class LocationTags(enum.Enum): BULLETIN_BOARD_BUNDLE = enum.auto() VAULT_BUNDLE = enum.auto() COMMUNITY_CENTER_ROOM = enum.auto() + RACCOON_BUNDLES = enum.auto() BACKPACK = enum.auto() TOOL_UPGRADE = enum.auto() HOE_UPGRADE = enum.auto() @@ -40,6 +41,7 @@ class LocationTags(enum.Enum): WATERING_CAN_UPGRADE = enum.auto() TRASH_CAN_UPGRADE = enum.auto() FISHING_ROD_UPGRADE = enum.auto() + PAN_UPGRADE = enum.auto() THE_MINES_TREASURE = enum.auto() CROPSANITY = enum.auto() ELEVATOR = enum.auto() @@ -49,6 +51,7 @@ class LocationTags(enum.Enum): FORAGING_LEVEL = enum.auto() COMBAT_LEVEL = enum.auto() MINING_LEVEL = enum.auto() + MASTERY_LEVEL = enum.auto() BUILDING_BLUEPRINT = enum.auto() STORY_QUEST = enum.auto() ARCADE_MACHINE = enum.auto() @@ -63,11 +66,18 @@ class LocationTags(enum.Enum): FRIENDSANITY = enum.auto() FESTIVAL = enum.auto() FESTIVAL_HARD = enum.auto() + DESERT_FESTIVAL_CHEF = enum.auto() SPECIAL_ORDER_BOARD = enum.auto() SPECIAL_ORDER_QI = enum.auto() REQUIRES_QI_ORDERS = enum.auto() + REQUIRES_MASTERIES = enum.auto() GINGER_ISLAND = enum.auto() WALNUT_PURCHASE = enum.auto() + WALNUTSANITY = enum.auto() + WALNUTSANITY_PUZZLE = enum.auto() + WALNUTSANITY_BUSH = enum.auto() + WALNUTSANITY_DIG = enum.auto() + WALNUTSANITY_REPEATABLE = enum.auto() BABY = enum.auto() MONSTERSANITY = enum.auto() @@ -87,6 +97,10 @@ class LocationTags(enum.Enum): CHEFSANITY_SKILL = enum.auto() CHEFSANITY_STARTER = enum.auto() CRAFTSANITY = enum.auto() + BOOKSANITY = enum.auto() + BOOKSANITY_POWER = enum.auto() + BOOKSANITY_SKILL = enum.auto() + BOOKSANITY_LOST = enum.auto() # Mods # Skill Mods LUCK_LEVEL = enum.auto() @@ -116,10 +130,7 @@ def __call__(self, name: str, code: Optional[int], region: str) -> None: def load_location_csv() -> List[LocationData]: - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files + from importlib.resources import files with files(data).joinpath("locations.csv").open() as file: reader = csv.DictReader(file) @@ -143,10 +154,10 @@ def load_location_csv() -> List[LocationData]: LocationData(None, Region.farm_house, Goal.full_house), LocationData(None, Region.island_west, Goal.greatest_walnut_hunter), LocationData(None, Region.adventurer_guild, Goal.protector_of_the_valley), - LocationData(None, Region.shipping, Goal.full_shipment), - LocationData(None, Region.kitchen, Goal.gourmet_chef), + LocationData(None, LogicRegion.shipping, Goal.full_shipment), + LocationData(None, LogicRegion.kitchen, Goal.gourmet_chef), LocationData(None, Region.farm, Goal.craft_master), - LocationData(None, Region.shipping, Goal.legend), + LocationData(None, LogicRegion.shipping, Goal.legend), LocationData(None, Region.farm, Goal.mystery_of_the_stardrops), LocationData(None, Region.farm, Goal.allsanity), LocationData(None, Region.qi_walnut_room, Goal.perfection), @@ -168,21 +179,21 @@ def initialize_groups(): initialize_groups() -def extend_cropsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - if options.cropsanity == Cropsanity.option_disabled: +def extend_cropsanity_locations(randomized_locations: List[LocationData], content: StardewContent): + cropsanity = content.features.cropsanity + if not cropsanity.is_enabled: return - cropsanity_locations = [item for item in locations_by_tag[LocationTags.CROPSANITY] if not item.mod_name or item.mod_name in options.mods] - cropsanity_locations = filter_ginger_island(options, cropsanity_locations) - randomized_locations.extend(cropsanity_locations) + randomized_locations.extend(location_table[cropsanity.to_location_name(item.name)] + for item in content.find_tagged_items(ItemTag.CROPSANITY)) -def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): if options.quest_locations < 0: return story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST] - story_quest_locations = filter_disabled_locations(options, story_quest_locations) + story_quest_locations = filter_disabled_locations(options, content, story_quest_locations) randomized_locations.extend(story_quest_locations) for i in range(0, options.quest_locations.value): @@ -199,32 +210,19 @@ def extend_quests_locations(randomized_locations: List[LocationData], options: S randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"]) -def extend_fishsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): - prefix = "Fishsanity: " - fishsanity = options.fishsanity - active_fish = get_fish_for_mods(options.mods.value) - if fishsanity == Fishsanity.option_none: +def extend_fishsanity_locations(randomized_locations: List[LocationData], content: StardewContent, random: Random): + fishsanity = content.features.fishsanity + if not fishsanity.is_enabled: return - elif fishsanity == Fishsanity.option_legendaries: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.legendary] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_special: - randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) - elif fishsanity == Fishsanity.option_randomized: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if random.random() < 0.4] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_all: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_exclude_legendaries: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if not fish.legendary] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_exclude_hard_fish: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.difficulty < 80] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif options.fishsanity == Fishsanity.option_only_easy_fish: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.difficulty < 50] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) + + for fish in content.fishes.values(): + if not fishsanity.is_included(fish): + continue + + if fishsanity.is_randomized and random.random() >= fishsanity.randomization_ratio: + continue + + randomized_locations.append(location_table[fishsanity.to_location_name(fish.name)]) def extend_museumsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): @@ -240,38 +238,20 @@ def extend_museumsanity_locations(randomized_locations: List[LocationData], opti randomized_locations.extend(location_table[f"{prefix}{museum_item.item_name}"] for museum_item in all_museum_items) -def extend_friendsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - island_villagers = [NPC.leo, ModNPC.lance] - if options.friendsanity == Friendsanity.option_none: +def extend_friendsanity_locations(randomized_locations: List[LocationData], content: StardewContent): + friendsanity = content.features.friendsanity + if not friendsanity.is_enabled: return randomized_locations.append(location_table[f"Spouse Stardrop"]) extend_baby_locations(randomized_locations) - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors - exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ - options.friendsanity == Friendsanity.option_bachelors - include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage - heart_size = options.friendsanity_heart_size - for villager in get_villagers_for_mods(options.mods.value): - if not villager.available and exclude_locked_villagers: - continue - if not villager.bachelor and exclude_non_bachelors: - continue - if villager.name in island_villagers and exclude_ginger_island: - continue - heart_cap = 8 if villager.bachelor else 10 - if include_post_marriage_hearts and villager.bachelor: - heart_cap = 14 - for heart in range(1, 15): - if heart > heart_cap: - break - if heart % heart_size == 0 or heart == heart_cap: - randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"]) - if not exclude_non_bachelors: - for heart in range(1, 6): - if heart % heart_size == 0 or heart == 5: - randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) + + for villager in content.villagers.values(): + for heart in friendsanity.get_randomized_hearts(villager): + randomized_locations.append(location_table[friendsanity.to_location_name(villager.name, heart)]) + + for heart in friendsanity.get_pet_randomized_hearts(): + randomized_locations.append(location_table[friendsanity.to_location_name(NPC.pet, heart)]) def extend_baby_locations(randomized_locations: List[LocationData]): @@ -279,16 +259,17 @@ def extend_baby_locations(randomized_locations: List[LocationData]): randomized_locations.extend(baby_locations) -def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): if options.festival_locations == FestivalLocations.option_disabled: return festival_locations = locations_by_tag[LocationTags.FESTIVAL] randomized_locations.extend(festival_locations) extend_hard_festival_locations(randomized_locations, options) + extend_desert_festival_chef_locations(randomized_locations, options, random) -def extend_hard_festival_locations(randomized_locations, options: StardewValleyOptions): +def extend_hard_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): if options.festival_locations != FestivalLocations.option_hard: return @@ -296,14 +277,20 @@ def extend_hard_festival_locations(randomized_locations, options: StardewValleyO randomized_locations.extend(hard_festival_locations) -def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - if options.special_order_locations == SpecialOrderLocations.option_disabled: - return +def extend_desert_festival_chef_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): + festival_chef_locations = locations_by_tag[LocationTags.DESERT_FESTIVAL_CHEF] + number_to_add = 5 if options.festival_locations == FestivalLocations.option_easy else 10 + locations_to_add = random.sample(festival_chef_locations, number_to_add) + randomized_locations.extend(locations_to_add) + + +def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): + if options.special_order_locations & SpecialOrderLocations.option_board: + board_locations = filter_disabled_locations(options, content, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) + randomized_locations.extend(board_locations) include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false - board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) - randomized_locations.extend(board_locations) - if options.special_order_locations == SpecialOrderLocations.option_board_qi and include_island: + if options.special_order_locations & SpecialOrderLocations.value_qi and include_island: include_arcade = options.arcade_machine_locations != ArcadeMachineLocations.option_disabled qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if include_arcade or LocationTags.JUNIMO_KART not in location.tags] @@ -321,9 +308,9 @@ def extend_walnut_purchase_locations(randomized_locations: List[LocationData], o randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE]) -def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]] - filtered_mandatory_locations = filter_disabled_locations(options, mandatory_locations) + filtered_mandatory_locations = filter_disabled_locations(options, content, mandatory_locations) randomized_locations.extend(filtered_mandatory_locations) @@ -362,32 +349,32 @@ def extend_elevator_locations(randomized_locations: List[LocationData], options: randomized_locations.extend(filtered_elevator_locations) -def extend_monstersanity_locations(randomized_locations: List[LocationData], options): +def extend_monstersanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): monstersanity = options.monstersanity if monstersanity == Monstersanity.option_none: return if monstersanity == Monstersanity.option_one_per_monster or monstersanity == Monstersanity.option_split_goals: monster_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_MONSTER]] - filtered_monster_locations = filter_disabled_locations(options, monster_locations) + filtered_monster_locations = filter_disabled_locations(options, content, monster_locations) randomized_locations.extend(filtered_monster_locations) return goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_GOALS]] - filtered_goal_locations = filter_disabled_locations(options, goal_locations) + filtered_goal_locations = filter_disabled_locations(options, content, goal_locations) randomized_locations.extend(filtered_goal_locations) if monstersanity != Monstersanity.option_progressive_goals: return progressive_goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_PROGRESSIVE_GOALS]] - filtered_progressive_goal_locations = filter_disabled_locations(options, progressive_goal_locations) + filtered_progressive_goal_locations = filter_disabled_locations(options, content, progressive_goal_locations) randomized_locations.extend(filtered_progressive_goal_locations) -def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): shipsanity = options.shipsanity if shipsanity == Shipsanity.option_none: return if shipsanity == Shipsanity.option_everything: ship_locations = [location for location in locations_by_tag[LocationTags.SHIPSANITY]] - filtered_ship_locations = filter_disabled_locations(options, ship_locations) + filtered_ship_locations = filter_disabled_locations(options, content, ship_locations) randomized_locations.extend(filtered_ship_locations) return shipsanity_locations = set() @@ -398,11 +385,11 @@ def extend_shipsanity_locations(randomized_locations: List[LocationData], option if shipsanity == Shipsanity.option_full_shipment or shipsanity == Shipsanity.option_full_shipment_with_fish: shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]}) - filtered_shipsanity_locations = filter_disabled_locations(options, list(shipsanity_locations)) + filtered_shipsanity_locations = filter_disabled_locations(options, content, list(shipsanity_locations)) randomized_locations.extend(filtered_shipsanity_locations) -def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): cooksanity = options.cooksanity if cooksanity == Cooksanity.option_none: return @@ -411,11 +398,11 @@ def extend_cooksanity_locations(randomized_locations: List[LocationData], option else: cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY]) - filtered_cooksanity_locations = filter_disabled_locations(options, cooksanity_locations) + filtered_cooksanity_locations = filter_disabled_locations(options, content, cooksanity_locations) randomized_locations.extend(filtered_cooksanity_locations) -def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): chefsanity = options.chefsanity if chefsanity == Chefsanity.option_none: return @@ -431,26 +418,56 @@ def extend_chefsanity_locations(randomized_locations: List[LocationData], option if chefsanity & Chefsanity.option_skills: chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_SKILL]}) - filtered_chefsanity_locations = filter_disabled_locations(options, list(chefsanity_locations_by_name.values())) + filtered_chefsanity_locations = filter_disabled_locations(options, content, list(chefsanity_locations_by_name.values())) randomized_locations.extend(filtered_chefsanity_locations) -def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent): if options.craftsanity == Craftsanity.option_none: return craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]] - filtered_chefsanity_locations = filter_disabled_locations(options, craftsanity_locations) - randomized_locations.extend(filtered_chefsanity_locations) + filtered_craftsanity_locations = filter_disabled_locations(options, content, craftsanity_locations) + randomized_locations.extend(filtered_craftsanity_locations) + + +def extend_book_locations(randomized_locations: List[LocationData], content: StardewContent): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + book_locations = [] + for book in content.find_tagged_items(ItemTag.BOOK): + if booksanity.is_included(book): + book_locations.append(location_table[booksanity.to_location_name(book.name)]) + + book_locations.extend(location_table[booksanity.to_location_name(book)] for book in booksanity.get_randomized_lost_books()) + + randomized_locations.extend(book_locations) + + +def extend_walnutsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if not options.walnutsanity: + return + + if "Puzzles" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_PUZZLE]) + if "Bushes" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_BUSH]) + if "Dig Spots" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_DIG]) + if "Repeatables" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_REPEATABLE]) def create_locations(location_collector: StardewLocationCollector, bundle_rooms: List[BundleRoom], options: StardewValleyOptions, + content: StardewContent, random: Random): randomized_locations = [] - extend_mandatory_locations(randomized_locations, options) + extend_mandatory_locations(randomized_locations, options, content) extend_bundle_locations(randomized_locations, bundle_rooms) extend_backpack_locations(randomized_locations, options) @@ -459,10 +476,12 @@ def create_locations(location_collector: StardewLocationCollector, extend_elevator_locations(randomized_locations, options) - if not options.skill_progression == SkillProgression.option_vanilla: - for location in locations_by_tag[LocationTags.SKILL_LEVEL]: - if location.mod_name is None or location.mod_name in options.mods: - randomized_locations.append(location_table[location.name]) + skill_progression = content.features.skill_progression + if skill_progression.is_progressive: + for skill in content.skills.values(): + randomized_locations.extend([location_table[location_name] for _, location_name in skill_progression.get_randomized_level_names_by_level(skill)]) + if skill_progression.is_mastery_randomized(skill): + randomized_locations.append(location_table[skill.mastery_name]) if options.building_progression & BuildingProgression.option_progressive: for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: @@ -475,43 +494,64 @@ def create_locations(location_collector: StardewLocationCollector, if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling: randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE]) - extend_cropsanity_locations(randomized_locations, options) - extend_fishsanity_locations(randomized_locations, options, random) + extend_cropsanity_locations(randomized_locations, content) + extend_fishsanity_locations(randomized_locations, content, random) extend_museumsanity_locations(randomized_locations, options, random) - extend_friendsanity_locations(randomized_locations, options) + extend_friendsanity_locations(randomized_locations, content) - extend_festival_locations(randomized_locations, options) - extend_special_order_locations(randomized_locations, options) + extend_festival_locations(randomized_locations, options, random) + extend_special_order_locations(randomized_locations, options, content) extend_walnut_purchase_locations(randomized_locations, options) - extend_monstersanity_locations(randomized_locations, options) - extend_shipsanity_locations(randomized_locations, options) - extend_cooksanity_locations(randomized_locations, options) - extend_chefsanity_locations(randomized_locations, options) - extend_craftsanity_locations(randomized_locations, options) - extend_quests_locations(randomized_locations, options) + extend_monstersanity_locations(randomized_locations, options, content) + extend_shipsanity_locations(randomized_locations, options, content) + extend_cooksanity_locations(randomized_locations, options, content) + extend_chefsanity_locations(randomized_locations, options, content) + extend_craftsanity_locations(randomized_locations, options, content) + extend_quests_locations(randomized_locations, options, content) + extend_book_locations(randomized_locations, content) + extend_walnutsanity_locations(randomized_locations, options) + + # Mods extend_situational_quest_locations(randomized_locations, options) for location_data in randomized_locations: location_collector(location_data.name, location_data.code, location_data.region) +def filter_farm_type(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: + # On Meadowlands, "Feeding Animals" replaces "Raising Animals" + if options.farm_type == FarmType.option_meadowlands: + return (location for location in locations if location.name != Quest.raising_animals) + else: + return (location for location in locations if location.name != Quest.feeding_animals) + + def filter_ginger_island(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false return (location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags) def filter_qi_order_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: - include_qi_orders = options.special_order_locations == SpecialOrderLocations.option_board_qi + include_qi_orders = options.special_order_locations & SpecialOrderLocations.value_qi return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags) +def filter_masteries_locations(content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]: + # FIXME Remove once recipes are handled by the content packs + if content.features.skill_progression.are_masteries_shuffled: + return locations + return (location for location in locations if LocationTags.REQUIRES_MASTERIES not in location.tags) + + def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: return (location for location in locations if location.mod_name is None or location.mod_name in options.mods) -def filter_disabled_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: - locations_island_filter = filter_ginger_island(options, locations) +def filter_disabled_locations(options: StardewValleyOptions, content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]: + locations_farm_filter = filter_farm_type(options, locations) + locations_island_filter = filter_ginger_island(options, locations_farm_filter) locations_qi_filter = filter_qi_order_locations(options, locations_island_filter) - locations_mod_filter = filter_modded_locations(options, locations_qi_filter) + locations_masteries_filter = filter_masteries_locations(content, locations_qi_filter) + locations_mod_filter = filter_modded_locations(options, locations_masteries_filter) return locations_mod_filter diff --git a/worlds/stardew_valley/logic/ability_logic.py b/worlds/stardew_valley/logic/ability_logic.py index ae12ffee4742..add99a2c2e7e 100644 --- a/worlds/stardew_valley/logic/ability_logic.py +++ b/worlds/stardew_valley/logic/ability_logic.py @@ -1,6 +1,7 @@ from typing import Union from .base_logic import BaseLogicMixin, BaseLogic +from .cooking_logic import CookingLogicMixin from .mine_logic import MineLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin diff --git a/worlds/stardew_valley/logic/action_logic.py b/worlds/stardew_valley/logic/action_logic.py index 820ae4ead429..dc5deda427f3 100644 --- a/worlds/stardew_valley/logic/action_logic.py +++ b/worlds/stardew_valley/logic/action_logic.py @@ -5,10 +5,13 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin -from ..stardew_rule import StardewRule, True_, Or +from .tool_logic import ToolLogicMixin +from ..options import ToolProgression +from ..stardew_rule import StardewRule, True_ from ..strings.generic_names import Generic from ..strings.geode_names import Geode from ..strings.region_names import Region +from ..strings.tool_names import Tool class ActionLogicMixin(BaseLogicMixin): @@ -17,7 +20,7 @@ def __init__(self, *args, **kwargs): self.action = ActionLogic(*args, **kwargs) -class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]): +class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, ToolLogicMixin]]): def can_watch(self, channel: str = None): tv_rule = True_() @@ -25,16 +28,13 @@ def can_watch(self, channel: str = None): return tv_rule return self.logic.received(channel) & tv_rule - def can_pan(self) -> StardewRule: - return self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) - - def can_pan_at(self, region: str) -> StardewRule: - return self.logic.region.can_reach(region) & self.logic.action.can_pan() + def can_pan_at(self, region: str, material: str) -> StardewRule: + return self.logic.region.can_reach(region) & self.logic.tool.has_tool(Tool.pan, material) @cache_self1 def can_open_geode(self, geode: str) -> StardewRule: blacksmith_access = self.logic.region.can_reach(Region.blacksmith) geodes = [Geode.geode, Geode.frozen, Geode.magma, Geode.omni] if geode == Generic.any: - return blacksmith_access & Or(*(self.logic.has(geode_type) for geode_type in geodes)) + return blacksmith_access & self.logic.or_(*(self.logic.has(geode_type) for geode_type in geodes)) return blacksmith_access & self.logic.has(geode) diff --git a/worlds/stardew_valley/logic/artisan_logic.py b/worlds/stardew_valley/logic/artisan_logic.py index cdc2186d807a..23f0ae03b790 100644 --- a/worlds/stardew_valley/logic/artisan_logic.py +++ b/worlds/stardew_valley/logic/artisan_logic.py @@ -3,8 +3,13 @@ from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin from .time_logic import TimeLogicMixin +from ..data.artisan import MachineSource +from ..data.game_item import ItemTag from ..stardew_rule import StardewRule -from ..strings.crop_names import all_vegetables, all_fruits, Vegetable, Fruit +from ..strings.artisan_good_names import ArtisanGood +from ..strings.crop_names import Vegetable, Fruit +from ..strings.fish_names import Fish, all_fish +from ..strings.forageable_names import Mushroom from ..strings.generic_names import Generic from ..strings.machine_names import Machine @@ -16,6 +21,10 @@ def __init__(self, *args, **kwargs): class ArtisanLogic(BaseLogic[Union[ArtisanLogicMixin, TimeLogicMixin, HasLogicMixin]]): + def initialize_rules(self): + # TODO remove this one too once fish are converted to sources + self.registry.artisan_good_rules.update({ArtisanGood.specific_smoked_fish(fish): self.can_smoke(fish) for fish in all_fish}) + self.registry.artisan_good_rules.update({ArtisanGood.specific_bait(fish): self.can_bait(fish) for fish in all_fish}) def has_jelly(self) -> StardewRule: return self.logic.artisan.can_preserves_jar(Fruit.any) @@ -23,31 +32,62 @@ def has_jelly(self) -> StardewRule: def has_pickle(self) -> StardewRule: return self.logic.artisan.can_preserves_jar(Vegetable.any) + def has_smoked_fish(self) -> StardewRule: + return self.logic.artisan.can_smoke(Fish.any) + + def has_targeted_bait(self) -> StardewRule: + return self.logic.artisan.can_bait(Fish.any) + + def has_dried_fruits(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Fruit.any) + + def has_dried_mushrooms(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Mushroom.any_edible) + + def has_raisins(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Fruit.grape) + + def can_produce_from(self, source: MachineSource) -> StardewRule: + return self.logic.has(source.item) & self.logic.has(source.machine) + def can_preserves_jar(self, item: str) -> StardewRule: machine_rule = self.logic.has(Machine.preserves_jar) if item == Generic.any: return machine_rule if item == Fruit.any: - return machine_rule & self.logic.has_any(*all_fruits) + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT))) if item == Vegetable.any: - return machine_rule & self.logic.has_any(*all_vegetables) + return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE))) return machine_rule & self.logic.has(item) - def has_wine(self) -> StardewRule: - return self.logic.artisan.can_keg(Fruit.any) - - def has_juice(self) -> StardewRule: - return self.logic.artisan.can_keg(Vegetable.any) - def can_keg(self, item: str) -> StardewRule: machine_rule = self.logic.has(Machine.keg) if item == Generic.any: return machine_rule if item == Fruit.any: - return machine_rule & self.logic.has_any(*all_fruits) + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT))) if item == Vegetable.any: - return machine_rule & self.logic.has_any(*all_vegetables) + return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE))) return machine_rule & self.logic.has(item) def can_mayonnaise(self, item: str) -> StardewRule: return self.logic.has(Machine.mayonnaise_machine) & self.logic.has(item) + + def can_smoke(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.fish_smoker) + return machine_rule & self.logic.has(item) + + def can_bait(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.bait_maker) + return machine_rule & self.logic.has(item) + + def can_dehydrate(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.dehydrator) + if item == Generic.any: + return machine_rule + if item == Fruit.any: + # Grapes make raisins + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT) if fruit.name != Fruit.grape)) + if item == Mushroom.any_edible: + return machine_rule & self.logic.has_any(*(mushroom.name for mushroom in self.content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM))) + return machine_rule & self.logic.has(item) diff --git a/worlds/stardew_valley/logic/base_logic.py b/worlds/stardew_valley/logic/base_logic.py index 9cfd089ea4f6..7b377fce1fcc 100644 --- a/worlds/stardew_valley/logic/base_logic.py +++ b/worlds/stardew_valley/logic/base_logic.py @@ -2,6 +2,7 @@ from typing import TypeVar, Generic, Dict, Collection +from ..content.game_content import StardewContent from ..options import StardewValleyOptions from ..stardew_rule import StardewRule @@ -10,12 +11,11 @@ class LogicRegistry: def __init__(self): self.item_rules: Dict[str, StardewRule] = {} - self.sapling_rules: Dict[str, StardewRule] = {} - self.tree_fruit_rules: Dict[str, StardewRule] = {} self.seed_rules: Dict[str, StardewRule] = {} self.cooking_rules: Dict[str, StardewRule] = {} self.crafting_rules: Dict[str, StardewRule] = {} self.crop_rules: Dict[str, StardewRule] = {} + self.artisan_good_rules: Dict[str, StardewRule] = {} self.fish_rules: Dict[str, StardewRule] = {} self.museum_rules: Dict[str, StardewRule] = {} self.festival_rules: Dict[str, StardewRule] = {} @@ -38,13 +38,15 @@ class BaseLogic(BaseLogicMixin, Generic[T]): player: int registry: LogicRegistry options: StardewValleyOptions + content: StardewContent regions: Collection[str] logic: T - def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, regions: Collection[str], logic: T): - super().__init__(player, registry, options, regions, logic) + def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, content: StardewContent, regions: Collection[str], logic: T): + super().__init__(player, registry, options, content, regions, logic) self.player = player self.registry = registry self.options = options + self.content = content self.regions = regions self.logic = logic diff --git a/worlds/stardew_valley/logic/book_logic.py b/worlds/stardew_valley/logic/book_logic.py new file mode 100644 index 000000000000..464056ee06ba --- /dev/null +++ b/worlds/stardew_valley/logic/book_logic.py @@ -0,0 +1,24 @@ +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from ..stardew_rule import StardewRule + + +class BookLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.book = BookLogic(*args, **kwargs) + + +class BookLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin]]): + + @cache_self1 + def has_book_power(self, book: str) -> StardewRule: + booksanity = self.content.features.booksanity + if booksanity.is_included(self.content.game_items[book]): + return self.logic.received(booksanity.to_item_name(book)) + else: + return self.logic.has(book) diff --git a/worlds/stardew_valley/logic/buff_logic.py b/worlds/stardew_valley/logic/buff_logic.py deleted file mode 100644 index fee9c9fc4d25..000000000000 --- a/worlds/stardew_valley/logic/buff_logic.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Union - -from .base_logic import BaseLogicMixin, BaseLogic -from .received_logic import ReceivedLogicMixin -from ..stardew_rule import StardewRule -from ..strings.ap_names.buff_names import Buff - - -class BuffLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.buff = BuffLogic(*args, **kwargs) - - -class BuffLogic(BaseLogic[Union[ReceivedLogicMixin]]): - def has_max_buffs(self) -> StardewRule: - return self.has_max_speed() & self.has_max_luck() - - def has_max_speed(self) -> StardewRule: - return self.logic.received(Buff.movement, self.options.movement_buff_number.value) - - def has_max_luck(self) -> StardewRule: - return self.logic.received(Buff.luck, self.options.luck_buff_number.value) diff --git a/worlds/stardew_valley/logic/building_logic.py b/worlds/stardew_valley/logic/building_logic.py index 7be3d19ec33b..b4eff4399385 100644 --- a/worlds/stardew_valley/logic/building_logic.py +++ b/worlds/stardew_valley/logic/building_logic.py @@ -1,3 +1,4 @@ +from functools import cached_property from typing import Dict, Union from Utils import cache_self1 @@ -8,12 +9,14 @@ from .region_logic import RegionLogicMixin from ..options import BuildingProgression from ..stardew_rule import StardewRule, True_, False_, Has -from ..strings.ap_names.event_names import Event from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building from ..strings.fish_names import WaterItem from ..strings.material_names import Material from ..strings.metal_names import MetalBar +from ..strings.region_names import Region + +has_group = "building" class BuildingLogicMixin(BaseLogicMixin): @@ -42,7 +45,7 @@ def initialize_rules(self): Building.well: self.logic.money.can_spend(1000) & self.logic.has(Material.stone), Building.shipping_bin: self.logic.money.can_spend(250) & self.logic.has(Material.wood), Building.kitchen: self.logic.money.can_spend(10000) & self.logic.has(Material.wood) & self.logic.building.has_house(0), - Building.kids_room: self.logic.money.can_spend(50000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1), + Building.kids_room: self.logic.money.can_spend(65000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1), Building.cellar: self.logic.money.can_spend(100000) & self.logic.building.has_house(2), # @formatter:on }) @@ -58,9 +61,9 @@ def has_building(self, building: str) -> StardewRule: return True_() return self.logic.received(building) - carpenter_rule = self.logic.received(Event.can_construct_buildings) + carpenter_rule = self.logic.building.can_construct_buildings if not self.options.building_progression & BuildingProgression.option_progressive: - return Has(building, self.registry.building_rules) & carpenter_rule + return Has(building, self.registry.building_rules, has_group) & carpenter_rule count = 1 if building in [Building.coop, Building.barn, Building.shed]: @@ -73,6 +76,10 @@ def has_building(self, building: str) -> StardewRule: building = " ".join(["Progressive", *building.split(" ")[1:]]) return self.logic.received(building, count) & carpenter_rule + @cached_property + def can_construct_buildings(self) -> StardewRule: + return self.logic.region.can_reach(Region.carpenter) + @cache_self1 def has_house(self, upgrade_level: int) -> StardewRule: if upgrade_level < 1: @@ -81,15 +88,15 @@ def has_house(self, upgrade_level: int) -> StardewRule: if upgrade_level > 3: return False_() - carpenter_rule = self.logic.received(Event.can_construct_buildings) + carpenter_rule = self.logic.building.can_construct_buildings if self.options.building_progression & BuildingProgression.option_progressive: return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level) if upgrade_level == 1: - return carpenter_rule & Has(Building.kitchen, self.registry.building_rules) + return carpenter_rule & Has(Building.kitchen, self.registry.building_rules, has_group) if upgrade_level == 2: - return carpenter_rule & Has(Building.kids_room, self.registry.building_rules) + return carpenter_rule & Has(Building.kids_room, self.registry.building_rules, has_group) # if upgrade_level == 3: - return carpenter_rule & Has(Building.cellar, self.registry.building_rules) + return carpenter_rule & Has(Building.cellar, self.registry.building_rules, has_group) diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 1ae07cf2ed82..98fda1c73c7d 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -2,17 +2,22 @@ from typing import Union, List from .base_logic import BaseLogicMixin, BaseLogic -from .farming_logic import FarmingLogicMixin from .fishing_logic import FishingLogicMixin from .has_logic import HasLogicMixin from .money_logic import MoneyLogicMixin +from .quality_logic import QualityLogicMixin +from .quest_logic import QuestLogicMixin +from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin from ..bundles.bundle import Bundle -from ..stardew_rule import StardewRule, And, True_ +from ..stardew_rule import StardewRule, True_ +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade from ..strings.currency_names import Currency from ..strings.machine_names import Machine from ..strings.quality_names import CropQuality, ForageQuality, FishQuality, ArtisanQuality +from ..strings.quest_names import Quest from ..strings.region_names import Region @@ -22,21 +27,26 @@ def __init__(self, *args, **kwargs): self.bundle = BundleLogic(*args, **kwargs) -class BundleLogic(BaseLogic[Union[HasLogicMixin, RegionLogicMixin, MoneyLogicMixin, FarmingLogicMixin, FishingLogicMixin, SkillLogicMixin]]): +class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, +SkillLogicMixin, QuestLogicMixin]]): # Should be cached def can_complete_bundle(self, bundle: Bundle) -> StardewRule: item_rules = [] qualities = [] + time_to_grind = 0 can_speak_junimo = self.logic.region.can_reach(Region.wizard_tower) for bundle_item in bundle.items: - if Currency.is_currency(bundle_item.item_name): - return can_speak_junimo & self.logic.money.can_trade(bundle_item.item_name, bundle_item.amount) + if Currency.is_currency(bundle_item.get_item()): + return can_speak_junimo & self.logic.money.can_trade(bundle_item.get_item(), bundle_item.amount) - item_rules.append(bundle_item.item_name) + item_rules.append(bundle_item.get_item()) + if bundle_item.amount > 50: + time_to_grind = bundle_item.amount // 50 qualities.append(bundle_item.quality) quality_rules = self.get_quality_rules(qualities) item_rules = self.logic.has_n(*item_rules, count=bundle.number_required) - return can_speak_junimo & item_rules & quality_rules + time_rule = self.logic.time.has_lived_months(time_to_grind) + return can_speak_junimo & item_rules & quality_rules & time_rule def get_quality_rules(self, qualities: List[str]) -> StardewRule: crop_quality = CropQuality.get_highest(qualities) @@ -45,7 +55,7 @@ def get_quality_rules(self, qualities: List[str]) -> StardewRule: artisan_quality = ArtisanQuality.get_highest(qualities) quality_rules = [] if crop_quality != CropQuality.basic: - quality_rules.append(self.logic.farming.can_grow_crop_quality(crop_quality)) + quality_rules.append(self.logic.quality.can_grow_crop_quality(crop_quality)) if fish_quality != FishQuality.basic: quality_rules.append(self.logic.fishing.can_catch_quality_fish(fish_quality)) if forage_quality != ForageQuality.basic: @@ -54,7 +64,7 @@ def get_quality_rules(self, qualities: List[str]) -> StardewRule: quality_rules.append(self.logic.has(Machine.cask)) if not quality_rules: return True_() - return And(*quality_rules) + return self.logic.and_(*quality_rules) @cached_property def can_complete_community_center(self) -> StardewRule: @@ -64,3 +74,11 @@ def can_complete_community_center(self) -> StardewRule: self.logic.region.can_reach_location("Complete Bulletin Board") & self.logic.region.can_reach_location("Complete Vault") & self.logic.region.can_reach_location("Complete Boiler Room")) + + def can_access_raccoon_bundles(self) -> StardewRule: + if self.options.quest_locations < 0: + return self.logic.received(CommunityUpgrade.raccoon, 1) & self.logic.quest.can_complete_quest(Quest.giant_stump) + + # 1 - Break the tree + # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off + return self.logic.received(CommunityUpgrade.raccoon, 2) diff --git a/worlds/stardew_valley/logic/combat_logic.py b/worlds/stardew_valley/logic/combat_logic.py index ba825192a99e..849bf14b2203 100644 --- a/worlds/stardew_valley/logic/combat_logic.py +++ b/worlds/stardew_valley/logic/combat_logic.py @@ -3,10 +3,11 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from ..mods.logic.magic_logic import MagicLogicMixin -from ..stardew_rule import StardewRule, Or, False_ +from ..stardew_rule import StardewRule, False_ from ..strings.ap_names.ap_weapon_names import APWeapon from ..strings.performance_names import Performance @@ -19,7 +20,7 @@ def __init__(self, *args, **kwargs): self.combat = CombatLogic(*args, **kwargs) -class CombatLogic(BaseLogic[Union[CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]): +class CombatLogic(BaseLogic[Union[HasLogicMixin, CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]): @cache_self1 def can_fight_at_level(self, level: str) -> StardewRule: if level == Performance.basic: @@ -42,16 +43,20 @@ def has_any_weapon(self) -> StardewRule: @cached_property def has_decent_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 2) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 2) for weapon in valid_weapons)) @cached_property def has_good_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 3) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 3) for weapon in valid_weapons)) @cached_property def has_great_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 4) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 4) for weapon in valid_weapons)) @cached_property def has_galaxy_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 5) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 5) for weapon in valid_weapons)) + + @cached_property + def has_slingshot(self) -> StardewRule: + return self.logic.received(APWeapon.slingshot) diff --git a/worlds/stardew_valley/logic/cooking_logic.py b/worlds/stardew_valley/logic/cooking_logic.py index 51cc74d0517a..46f3bdc93f2f 100644 --- a/worlds/stardew_valley/logic/cooking_logic.py +++ b/worlds/stardew_valley/logic/cooking_logic.py @@ -19,8 +19,8 @@ from ..locations import locations_by_tag, LocationTags from ..options import Chefsanity from ..options import ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_, And -from ..strings.region_names import Region +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.region_names import LogicRegion from ..strings.skill_names import Skill from ..strings.tv_channel_names import Channel @@ -39,7 +39,7 @@ def can_cook_in_kitchen(self) -> StardewRule: # Should be cached def can_cook(self, recipe: CookingRecipe = None) -> StardewRule: - cook_rule = self.logic.region.can_reach(Region.kitchen) + cook_rule = self.logic.region.can_reach(LogicRegion.kitchen) if recipe is None: return cook_rule @@ -65,7 +65,7 @@ def knows_recipe(self, source: RecipeSource, meal_name: str) -> StardewRule: return self.logic.cooking.received_recipe(meal_name) if isinstance(source, QueenOfSauceSource) and self.options.chefsanity & Chefsanity.option_queen_of_sauce: return self.logic.cooking.received_recipe(meal_name) - if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_friendship: + if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_purchases: return self.logic.cooking.received_recipe(meal_name) return self.logic.cooking.can_learn_recipe(source) @@ -105,4 +105,4 @@ def can_cook_everything(self) -> StardewRule: continue all_recipes_names.append(location.name[len(cooksanity_prefix):]) all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names] - return And(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes)) + return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes)) diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index 8c267b7d1090..28bf0d2af22c 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -13,12 +13,11 @@ from .special_order_logic import SpecialOrderLogicMixin from .. import options from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name -from ..data.recipe_data import StarterSource, ShopSource, SkillSource, FriendshipSource from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ - FestivalShopSource, QuestSource + FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource from ..locations import locations_by_tag, LocationTags from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_, And +from ..stardew_rule import StardewRule, True_, False_ from ..strings.region_names import Region @@ -55,10 +54,9 @@ def knows_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.crafting.received_recipe(recipe.item) if self.options.craftsanity == Craftsanity.option_none: return self.logic.crafting.can_learn_recipe(recipe) - if isinstance(recipe.source, StarterSource) or isinstance(recipe.source, ShopTradeSource) or isinstance( - recipe.source, ShopSource): + if isinstance(recipe.source, (StarterSource, ShopTradeSource, ShopSource, SkillCraftsanitySource)): return self.logic.crafting.received_recipe(recipe.item) - if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations != SpecialOrderLocations.option_disabled: + if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations & SpecialOrderLocations.option_board: return self.logic.crafting.received_recipe(recipe.item) return self.logic.crafting.can_learn_recipe(recipe) @@ -72,8 +70,12 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.money.can_trade_at(recipe.source.region, recipe.source.currency, recipe.source.price) if isinstance(recipe.source, ShopSource): return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price) + if isinstance(recipe.source, SkillCraftsanitySource): + return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill, recipe.source.level) if isinstance(recipe.source, SkillSource): return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) + if isinstance(recipe.source, MasterySource): + return self.logic.skill.has_mastery(recipe.source.skill) if isinstance(recipe.source, CutsceneSource): return self.logic.region.can_reach(recipe.source.region) & self.logic.relationship.has_hearts(recipe.source.friend, recipe.source.hearts) if isinstance(recipe.source, FriendshipSource): @@ -81,9 +83,9 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: if isinstance(recipe.source, QuestSource): return self.logic.quest.can_complete_quest(recipe.source.quest) if isinstance(recipe.source, SpecialOrderSource): - if self.options.special_order_locations == SpecialOrderLocations.option_disabled: - return self.logic.special_order.can_complete_special_order(recipe.source.special_order) - return self.logic.crafting.received_recipe(recipe.item) + if self.options.special_order_locations & SpecialOrderLocations.option_board: + return self.logic.crafting.received_recipe(recipe.item) + return self.logic.special_order.can_complete_special_order(recipe.source.special_order) if isinstance(recipe.source, LogicSource): if recipe.source.logic_rule == "Cellar": return self.logic.region.can_reach(Region.cellar) @@ -99,13 +101,17 @@ def can_craft_everything(self) -> StardewRule: craftsanity_prefix = "Craft " all_recipes_names = [] exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true + exclude_masteries = not self.content.features.skill_progression.are_masteries_shuffled for location in locations_by_tag[LocationTags.CRAFTSANITY]: if not location.name.startswith(craftsanity_prefix): continue if exclude_island and LocationTags.GINGER_ISLAND in location.tags: continue + # FIXME Remove when recipes are in content packs + if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags: + continue if location.mod_name and location.mod_name not in self.options.mods: continue all_recipes_names.append(location.name[len(craftsanity_prefix):]) all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names] - return And(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes)) + return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes)) diff --git a/worlds/stardew_valley/logic/crop_logic.py b/worlds/stardew_valley/logic/crop_logic.py deleted file mode 100644 index 8c107ba6a5df..000000000000 --- a/worlds/stardew_valley/logic/crop_logic.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Union, Iterable - -from Utils import cache_self1 -from .base_logic import BaseLogicMixin, BaseLogic -from .has_logic import HasLogicMixin -from .money_logic import MoneyLogicMixin -from .received_logic import ReceivedLogicMixin -from .region_logic import RegionLogicMixin -from .season_logic import SeasonLogicMixin -from .tool_logic import ToolLogicMixin -from .traveling_merchant_logic import TravelingMerchantLogicMixin -from ..data import CropItem, SeedItem -from ..options import Cropsanity, ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_ -from ..strings.craftable_names import Craftable -from ..strings.forageable_names import Forageable -from ..strings.machine_names import Machine -from ..strings.metal_names import Fossil -from ..strings.region_names import Region -from ..strings.seed_names import Seed -from ..strings.tool_names import Tool - - -class CropLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.crop = CropLogic(*args, **kwargs) - - -class CropLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, SeasonLogicMixin, MoneyLogicMixin, - ToolLogicMixin, CropLogicMixin]]): - @cache_self1 - def can_grow(self, crop: CropItem) -> StardewRule: - season_rule = self.logic.season.has_any(crop.farm_growth_seasons) - seed_rule = self.logic.has(crop.seed.name) - farm_rule = self.logic.region.can_reach(Region.farm) & season_rule - tool_rule = self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.has_tool(Tool.watering_can) - region_rule = farm_rule | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - if crop.name == Forageable.cactus_fruit: - region_rule = self.logic.region.can_reach(Region.greenhouse) | self.logic.has(Craftable.garden_pot) - return seed_rule & region_rule & tool_rule - - def can_plant_and_grow_item(self, seasons: Union[str, Iterable[str]]) -> StardewRule: - if isinstance(seasons, str): - seasons = [seasons] - season_rule = self.logic.season.has_any(seasons) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - farm_rule = self.logic.region.can_reach(Region.farm) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - return season_rule & farm_rule - - def has_island_farm(self) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_false: - return self.logic.region.can_reach(Region.island_west) - return False_() - - @cache_self1 - def can_buy_seed(self, seed: SeedItem) -> StardewRule: - if seed.requires_island and self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - return False_() - if self.options.cropsanity == Cropsanity.option_disabled or seed.name == Seed.qi_bean: - item_rule = True_() - else: - item_rule = self.logic.received(seed.name) - if seed.name == Seed.coffee: - item_rule = item_rule & self.logic.traveling_merchant.has_days(3) - season_rule = self.logic.season.has_any(seed.seasons) - region_rule = self.logic.region.can_reach_all(seed.regions) - currency_rule = self.logic.money.can_spend(1000) - if seed.name == Seed.pineapple: - currency_rule = self.logic.has(Forageable.magma_cap) - if seed.name == Seed.taro: - currency_rule = self.logic.has(Fossil.bone_fragment) - return season_rule & region_rule & item_rule & currency_rule diff --git a/worlds/stardew_valley/logic/farming_logic.py b/worlds/stardew_valley/logic/farming_logic.py index b255aa27f785..88523bb85d8e 100644 --- a/worlds/stardew_valley/logic/farming_logic.py +++ b/worlds/stardew_valley/logic/farming_logic.py @@ -1,11 +1,27 @@ -from typing import Union +from functools import cached_property +from typing import Union, Tuple +from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .has_logic import HasLogicMixin -from .skill_logic import SkillLogicMixin -from ..stardew_rule import StardewRule, True_, False_ +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin +from .tool_logic import ToolLogicMixin +from .. import options +from ..stardew_rule import StardewRule, True_, false_ +from ..strings.ap_names.event_names import Event from ..strings.fertilizer_names import Fertilizer -from ..strings.quality_names import CropQuality +from ..strings.region_names import Region +from ..strings.season_names import Season +from ..strings.tool_names import Tool + +farming_event_by_season = { + Season.spring: Event.spring_farming, + Season.summer: Event.summer_farming, + Season.fall: Event.fall_farming, + Season.winter: Event.winter_farming, +} class FarmingLogicMixin(BaseLogicMixin): @@ -14,7 +30,12 @@ def __init__(self, *args, **kwargs): self.farming = FarmingLogic(*args, **kwargs) -class FarmingLogic(BaseLogic[Union[HasLogicMixin, SkillLogicMixin, FarmingLogicMixin]]): +class FarmingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, FarmingLogicMixin]]): + + @cached_property + def has_farming_tools(self) -> StardewRule: + return self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.can_water(0) + def has_fertilizer(self, tier: int) -> StardewRule: if tier <= 0: return True_() @@ -25,17 +46,17 @@ def has_fertilizer(self, tier: int) -> StardewRule: if tier >= 3: return self.logic.has(Fertilizer.deluxe) - def can_grow_crop_quality(self, quality: str) -> StardewRule: - if quality == CropQuality.basic: - return True_() - if quality == CropQuality.silver: - return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | ( - self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3) - if quality == CropQuality.gold: - return self.logic.skill.has_farming_level(10) | ( - self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | ( - self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | ( - self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2)) - if quality == CropQuality.iridium: - return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4) - return False_() + @cache_self1 + def can_plant_and_grow_item(self, seasons: Union[str, Tuple[str]]) -> StardewRule: + if seasons == (): # indoor farming + return (self.logic.region.can_reach(Region.greenhouse) | self.logic.farming.has_island_farm()) & self.logic.farming.has_farming_tools + + if isinstance(seasons, str): + seasons = (seasons,) + + return self.logic.or_(*(self.logic.received(farming_event_by_season[season]) for season in seasons)) + + def has_island_farm(self) -> StardewRule: + if self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_false: + return self.logic.region.can_reach(Region.island_west) + return false_ 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/fishing_logic.py b/worlds/stardew_valley/logic/fishing_logic.py index a7399a65d99c..63798a92fe9e 100644 --- a/worlds/stardew_valley/logic/fishing_logic.py +++ b/worlds/stardew_valley/logic/fishing_logic.py @@ -1,18 +1,22 @@ -from typing import Union, List +from typing import Union from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from .skill_logic import SkillLogicMixin from .tool_logic import ToolLogicMixin -from ..data import FishItem, fish_data -from ..locations import LocationTags, locations_by_tag -from ..options import ExcludeGingerIsland, Fishsanity +from ..data import fish_data +from ..data.fish_data import FishItem +from ..options import ExcludeGingerIsland from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, True_, False_, And +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.ap_names.mods.mod_items import SVEQuestItem +from ..strings.craftable_names import Fishing from ..strings.fish_names import SVEFish +from ..strings.machine_names import Machine from ..strings.quality_names import FishQuality from ..strings.region_names import Region from ..strings.skill_names import Skill @@ -24,17 +28,16 @@ def __init__(self, *args, **kwargs): self.fishing = FishingLogic(*args, **kwargs) -class FishingLogic(BaseLogic[Union[FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, SkillLogicMixin]]): +class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, + SkillLogicMixin]]): def can_fish_in_freshwater(self) -> StardewRule: return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain)) def has_max_fishing(self) -> StardewRule: - skill_rule = self.logic.skill.has_level(Skill.fishing, 10) - return self.logic.tool.has_fishing_rod(4) & skill_rule + return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 10) def can_fish_chests(self) -> StardewRule: - skill_rule = self.logic.skill.has_level(Skill.fishing, 6) - return self.logic.tool.has_fishing_rod(4) & skill_rule + return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 6) def can_fish_at(self, region: str) -> StardewRule: return self.logic.skill.can_fish() & self.logic.region.can_reach(region) @@ -51,51 +54,62 @@ def can_catch_fish(self, fish: FishItem) -> StardewRule: else: difficulty_rule = self.logic.skill.can_fish(difficulty=(120 if fish.legendary else fish.difficulty)) if fish.name == SVEFish.kittyfish: - item_rule = self.logic.received("Kittyfish Spell") + item_rule = self.logic.received(SVEQuestItem.kittyfish_spell) else: item_rule = True_() return quest_rule & region_rule & season_rule & difficulty_rule & item_rule + def can_catch_fish_for_fishsanity(self, fish: FishItem) -> StardewRule: + """ Rule could be different from the basic `can_catch_fish`. Imagine a fishsanity setting where you need to catch every fish with gold quality. + """ + return self.logic.fishing.can_catch_fish(fish) + def can_start_extended_family_quest(self) -> StardewRule: if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() - if self.options.special_order_locations != SpecialOrderLocations.option_board_qi: + if not self.options.special_order_locations & SpecialOrderLocations.value_qi: return False_() - return self.logic.region.can_reach(Region.qi_walnut_room) & And(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.legendary_fish)) + return (self.logic.region.can_reach(Region.qi_walnut_room) & + self.logic.and_(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.vanilla_legendary_fish))) def can_catch_quality_fish(self, fish_quality: str) -> StardewRule: if fish_quality == FishQuality.basic: return True_() - rod_rule = self.logic.tool.has_fishing_rod(2) if fish_quality == FishQuality.silver: - return rod_rule + return self.logic.tool.has_fishing_rod(2) if fish_quality == FishQuality.gold: - return rod_rule & self.logic.skill.has_level(Skill.fishing, 4) + return self.logic.skill.has_level(Skill.fishing, 4) & self.can_use_tackle(Fishing.quality_bobber) if fish_quality == FishQuality.iridium: - return rod_rule & self.logic.skill.has_level(Skill.fishing, 10) + return self.logic.skill.has_level(Skill.fishing, 10) & self.can_use_tackle(Fishing.quality_bobber) raise ValueError(f"Quality {fish_quality} is unknown.") + def can_use_tackle(self, tackle: str) -> StardewRule: + return self.logic.tool.has_fishing_rod(4) & self.logic.has(tackle) + def can_catch_every_fish(self) -> StardewRule: rules = [self.has_max_fishing()] - exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_extended_family = self.options.special_order_locations != SpecialOrderLocations.option_board_qi - for fish in fish_data.get_fish_for_mods(self.options.mods.value): - if exclude_island and fish in fish_data.island_fish: - continue - if exclude_extended_family and fish in fish_data.extended_family: - continue - rules.append(self.logic.fishing.can_catch_fish(fish)) - return And(*rules) - - def can_catch_every_fish_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule: - if self.options.fishsanity == Fishsanity.option_none: + + rules.extend( + self.logic.fishing.can_catch_fish(fish) + for fish in self.content.fishes.values() + ) + + return self.logic.and_(*rules) + + def can_catch_every_fish_for_fishsanity(self) -> StardewRule: + if not self.content.features.fishsanity.is_enabled: return self.can_catch_every_fish() rules = [self.has_max_fishing()] - for fishsanity_location in locations_by_tag[LocationTags.FISHSANITY]: - if fishsanity_location.name not in all_location_names_in_slot: - continue - rules.append(self.logic.region.can_reach_location(fishsanity_location.name)) - return And(*rules) + rules.extend( + self.logic.fishing.can_catch_fish_for_fishsanity(fish) + for fish in self.content.fishes.values() + if self.content.features.fishsanity.is_included(fish) + ) + + return self.logic.and_(*rules) + + def has_specific_bait(self, fish: FishItem) -> StardewRule: + return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker) diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py new file mode 100644 index 000000000000..997300ae7a54 --- /dev/null +++ b/worlds/stardew_valley/logic/grind_logic.py @@ -0,0 +1,77 @@ +from typing import Union, TYPE_CHECKING + +from Utils import cache_self1 +from .base_logic import BaseLogic, BaseLogicMixin +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 ..stardew_rule import StardewRule, HasProgressionPercent +from ..strings.book_names import Book +from ..strings.craftable_names import Consumable +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: + from .tool_logic import ToolLogicMixin +else: + ToolLogicMixin = object + +MIN_ITEMS = 10 +MAX_ITEMS = 999 +PERCENT_REQUIRED_FOR_MAX_ITEM = 24 + + +class GrindLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.grind = GrindLogic(*args, **kwargs) + + +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 not self.content.features.booksanity.is_enabled \ + 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_(opening_rule, mystery_box_rule, + book_of_mysteries_rule, time_rule,) + + def can_grind_artifact_troves(self, quantity: int) -> StardewRule: + 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: + 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)) + + def can_grind_fishing_treasure_chests(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.has(WaterChest.fishing_chest), + # Assuming one per week if the player does not grind it. + self.logic.time.has_lived_months(quantity // 4)) + + def can_grind_artifact_spots(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.tool.has_tool(Tool.hoe), + # Assuming twelve per month if the player does not grind it. + self.logic.time.has_lived_months(quantity // 12)) + + @cache_self1 + def can_grind_item(self, quantity: int) -> StardewRule: + if quantity <= MIN_ITEMS: + return self.logic.true_ + + quantity = min(quantity, MAX_ITEMS) + price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_ITEMS) + return HasProgressionPercent(self.player, price) diff --git a/worlds/stardew_valley/logic/harvesting_logic.py b/worlds/stardew_valley/logic/harvesting_logic.py new file mode 100644 index 000000000000..3b4d41953ccd --- /dev/null +++ b/worlds/stardew_valley/logic/harvesting_logic.py @@ -0,0 +1,56 @@ +from functools import cached_property +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .farming_logic import FarmingLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ..stardew_rule import StardewRule +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade +from ..strings.region_names import Region + + +class HarvestingLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.harvesting = HarvestingLogic(*args, **kwargs) + + +class HarvestingLogic(BaseLogic[Union[HarvestingLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, +FarmingLogicMixin, TimeLogicMixin]]): + + @cached_property + def can_harvest_from_fruit_bats(self) -> StardewRule: + return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.fruit_bats) + + @cached_property + def can_harvest_from_mushroom_cave(self) -> StardewRule: + return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.mushroom_boxes) + + @cache_self1 + def can_forage_from(self, source: ForagingSource) -> StardewRule: + seasons_rule = self.logic.season.has_any(source.seasons) + regions_rule = self.logic.region.can_reach_any(source.regions) + return seasons_rule & regions_rule + + @cache_self1 + def can_harvest_tree_from(self, source: HarvestFruitTreeSource) -> StardewRule: + # FIXME tool not required for this + region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons) + sapling_rule = self.logic.has(source.sapling) + # Because it takes 1 month to grow the sapling + time_rule = self.logic.time.has_lived_months(1) + + return region_to_grow_rule & sapling_rule & time_rule + + @cache_self1 + def can_harvest_crop_from(self, source: HarvestCropSource) -> StardewRule: + region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons) + seed_rule = self.logic.has(source.seed) + return region_to_grow_rule & seed_rule diff --git a/worlds/stardew_valley/logic/has_logic.py b/worlds/stardew_valley/logic/has_logic.py index d92d4224d7d2..4331780dc01d 100644 --- a/worlds/stardew_valley/logic/has_logic.py +++ b/worlds/stardew_valley/logic/has_logic.py @@ -1,8 +1,11 @@ from .base_logic import BaseLogic -from ..stardew_rule import StardewRule, And, Or, Has, Count +from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_ class HasLogicMixin(BaseLogic[None]): + true_ = true_ + false_ = false_ + # Should be cached def has(self, item: str) -> StardewRule: return Has(item, self.registry.item_rules) @@ -10,12 +13,12 @@ def has(self, item: str) -> StardewRule: def has_all(self, *items: str): assert items, "Can't have all of no items." - return And(*(self.has(item) for item in items)) + return self.logic.and_(*(self.has(item) for item in items)) def has_any(self, *items: str): assert items, "Can't have any of no items." - return Or(*(self.has(item) for item in items)) + return self.logic.or_(*(self.has(item) for item in items)) def has_n(self, *items: str, count: int): return self.count(count, *(self.has(item) for item in items)) @@ -24,6 +27,16 @@ def has_n(self, *items: str, count: int): def count(count: int, *rules: StardewRule) -> StardewRule: assert rules, "Can't create a Count conditions without rules" assert len(rules) >= count, "Count need at least as many rules as the count" + assert count > 0, "Count can't be negative" + + count -= sum(r is true_ for r in rules) + rules = list(r for r in rules if r is not true_) + + if count <= 0: + return true_ + + if len(rules) == 1: + return rules[0] if count == 1: return Or(*rules) @@ -31,4 +44,22 @@ def count(count: int, *rules: StardewRule) -> StardewRule: if count == len(rules): return And(*rules) - return Count(list(rules), count) + return Count(rules, count) + + @staticmethod + def and_(*rules: StardewRule) -> StardewRule: + assert rules, "Can't create a And conditions without rules" + + if len(rules) == 1: + return rules[0] + + return And(*rules) + + @staticmethod + def or_(*rules: StardewRule) -> StardewRule: + assert rules, "Can't create a Or conditions without rules" + + if len(rules) == 1: + return rules[0] + + return Or(*rules) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index a7fcec922838..6efc1ade4980 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -1,7 +1,7 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Collection +import logging +from typing import Collection, Callable from .ability_logic import AbilityLogicMixin from .action_logic import ActionLogicMixin @@ -9,58 +9,60 @@ from .arcade_logic import ArcadeLogicMixin from .artisan_logic import ArtisanLogicMixin from .base_logic import LogicRegistry -from .buff_logic import BuffLogicMixin +from .book_logic import BookLogicMixin from .building_logic import BuildingLogicMixin from .bundle_logic import BundleLogicMixin from .combat_logic import CombatLogicMixin from .cooking_logic import CookingLogicMixin from .crafting_logic import CraftingLogicMixin -from .crop_logic import CropLogicMixin 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 +from .harvesting_logic import HarvestingLogicMixin from .has_logic import HasLogicMixin +from .logic_event import all_logic_events from .mine_logic import MineLogicMixin from .money_logic import MoneyLogicMixin from .monster_logic import MonsterLogicMixin from .museum_logic import MuseumLogicMixin from .pet_logic import PetLogicMixin +from .quality_logic import QualityLogicMixin from .quest_logic import QuestLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .relationship_logic import RelationshipLogicMixin +from .requirement_logic import RequirementLogicMixin from .season_logic import SeasonLogicMixin from .shipping_logic import ShippingLogicMixin from .skill_logic import SkillLogicMixin +from .source_logic import SourceLogicMixin from .special_order_logic import SpecialOrderLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .traveling_merchant_logic import TravelingMerchantLogicMixin from .wallet_logic import WalletLogicMixin -from ..data import all_purchasable_seeds, all_crops +from .walnut_logic import WalnutLogicMixin +from ..content.game_content import StardewContent from ..data.craftable_data import all_crafting_recipes -from ..data.crops_data import crops_by_name -from ..data.fish_data import get_fish_for_mods from ..data.museum_data import all_museum_items from ..data.recipe_data import all_cooking_recipes from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_logic import ModLogicMixin from ..mods.mod_data import ModNames -from ..options import Cropsanity, SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, Fishsanity, Friendsanity, StardewValleyOptions -from ..stardew_rule import False_, Or, True_, And, StardewRule +from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions +from ..stardew_rule import False_, True_, StardewRule from ..strings.animal_names import Animal from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.ap_weapon_names import APWeapon -from ..strings.ap_names.buff_names import Buff from ..strings.ap_names.community_upgrade_names import CommunityUpgrade from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building -from ..strings.craftable_names import Consumable, Furniture, Ring, Fishing, Lighting, WildSeeds +from ..strings.craftable_names import Consumable, Ring, Fishing, Lighting, WildSeeds from ..strings.crop_names import Fruit, Vegetable 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 @@ -72,10 +74,10 @@ from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine from ..strings.material_names import Material -from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil +from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil, Artifact from ..strings.monster_drop_names import Loot from ..strings.monster_names import Monster -from ..strings.region_names import Region +from ..strings.region_names import Region, LogicRegion from ..strings.season_names import Season from ..strings.seed_names import Seed, TreeSeed from ..strings.skill_names import Skill @@ -83,23 +85,26 @@ from ..strings.villager_names import NPC from ..strings.wallet_item_names import Wallet +logger = logging.getLogger(__name__) -@dataclass(frozen=False, repr=False) -class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin, + +class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin, SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, GiftLogicMixin, BuildingLogicMixin, ShippingLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, WalletLogicMixin, AnimalLogicMixin, - CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, CropLogicMixin, + CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, - SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin): + SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions + content: StardewContent regions: Collection[str] - def __init__(self, player: int, options: StardewValleyOptions, regions: Collection[str]): + def __init__(self, player: int, options: StardewValleyOptions, content: StardewContent, regions: Collection[str]): self.registry = LogicRegistry() - super().__init__(player, self.registry, options, regions, self) + super().__init__(player, self.registry, options, content, regions, self) - self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value)}) + self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in content.fishes.values()}) self.registry.museum_rules.update({donation.item_name: self.museum.can_find_museum_item(donation) for donation in all_museum_items}) for recipe in all_cooking_recipes: @@ -118,37 +123,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti can_craft_rule = can_craft_rule | self.registry.crafting_rules[recipe.item] self.registry.crafting_rules[recipe.item] = can_craft_rule - self.registry.sapling_rules.update({ - Sapling.apple: self.can_buy_sapling(Fruit.apple), - Sapling.apricot: self.can_buy_sapling(Fruit.apricot), - Sapling.cherry: self.can_buy_sapling(Fruit.cherry), - Sapling.orange: self.can_buy_sapling(Fruit.orange), - Sapling.peach: self.can_buy_sapling(Fruit.peach), - Sapling.pomegranate: self.can_buy_sapling(Fruit.pomegranate), - Sapling.banana: self.can_buy_sapling(Fruit.banana), - Sapling.mango: self.can_buy_sapling(Fruit.mango), - }) - - self.registry.tree_fruit_rules.update({ - Fruit.apple: self.crop.can_plant_and_grow_item(Season.fall), - Fruit.apricot: self.crop.can_plant_and_grow_item(Season.spring), - Fruit.cherry: self.crop.can_plant_and_grow_item(Season.spring), - Fruit.orange: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.peach: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.pomegranate: self.crop.can_plant_and_grow_item(Season.fall), - Fruit.banana: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.mango: self.crop.can_plant_and_grow_item(Season.summer), - }) - - for tree_fruit in self.registry.tree_fruit_rules: - existing_rules = self.registry.tree_fruit_rules[tree_fruit] - sapling = f"{tree_fruit} Sapling" - self.registry.tree_fruit_rules[tree_fruit] = existing_rules & self.has(sapling) & self.time.has_lived_months(1) - - self.registry.seed_rules.update({seed.name: self.crop.can_buy_seed(seed) for seed in all_purchasable_seeds}) - self.registry.crop_rules.update({crop.name: self.crop.can_grow(crop) for crop in all_crops}) self.registry.crop_rules.update({ - Seed.coffee: (self.season.has(Season.spring) | self.season.has(Season.summer)) & self.crop.can_buy_seed(crops_by_name[Seed.coffee].seed), Fruit.ancient_fruit: (self.received("Ancient Seeds") | self.received("Ancient Seeds Recipe")) & self.region.can_reach(Region.greenhouse) & self.has(Machine.seed_maker), }) @@ -157,6 +132,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti self.registry.item_rules.update({ "Energy Tonic": self.money.can_spend_at(Region.hospital, 1000), WaterChest.fishing_chest: self.fishing.can_fish_chests(), + WaterChest.golden_fishing_chest: self.fishing.can_fish_chests() & self.skill.has_mastery(Skill.fishing), WaterChest.treasure: self.fishing.can_fish_chests(), Ring.hot_java_ring: self.region.can_reach(Region.volcano_floor_10), "Galaxy Soul": self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 40), @@ -197,7 +173,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti AnimalProduct.large_goat_milk: self.animal.has_happy_animal(Animal.goat), AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow), AnimalProduct.milk: self.animal.has_animal(Animal.cow), - AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True), + AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True) & self.has(Forageable.journal_scrap) & self.region.can_reach(Region.volcano_floor_5), AnimalProduct.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit), AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond), AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)), @@ -218,29 +194,35 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti ArtisanGood.dinosaur_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.dinosaur_egg), ArtisanGood.duck_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.duck_egg), ArtisanGood.goat_cheese: self.has(AnimalProduct.goat_milk) & self.has(Machine.cheese_press), - ArtisanGood.green_tea: self.artisan.can_keg(Vegetable.tea_leaves), ArtisanGood.honey: self.money.can_spend_at(Region.oasis, 200) | (self.has(Machine.bee_house) & self.season.has_any_not_winter()), - ArtisanGood.jelly: self.artisan.has_jelly(), - ArtisanGood.juice: self.artisan.has_juice(), ArtisanGood.maple_syrup: self.has(Machine.tapper), ArtisanGood.mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.chicken_egg), - ArtisanGood.mead: self.artisan.can_keg(ArtisanGood.honey), + ArtisanGood.mystic_syrup: self.has(Machine.tapper) & self.has(TreeSeed.mystic), ArtisanGood.oak_resin: self.has(Machine.tapper), - ArtisanGood.pale_ale: self.artisan.can_keg(Vegetable.hops), - ArtisanGood.pickles: self.artisan.has_pickle(), ArtisanGood.pine_tar: self.has(Machine.tapper), + ArtisanGood.smoked_fish: self.artisan.has_smoked_fish(), + ArtisanGood.targeted_bait: self.artisan.has_targeted_bait(), + ArtisanGood.stardrop_tea: self.has(WaterChest.golden_fishing_chest), ArtisanGood.truffle_oil: self.has(AnimalProduct.truffle) & self.has(Machine.oil_maker), ArtisanGood.void_mayonnaise: (self.skill.can_fish(Region.witch_swamp)) | (self.artisan.can_mayonnaise(AnimalProduct.void_egg)), - ArtisanGood.wine: self.artisan.has_wine(), - Beverage.beer: self.artisan.can_keg(Vegetable.wheat) | self.money.can_spend_at(Region.saloon, 400), - Beverage.coffee: self.artisan.can_keg(Seed.coffee) | self.has(Machine.coffee_maker) | (self.money.can_spend_at(Region.saloon, 300)) | self.has("Hot Java Ring"), Beverage.pina_colada: self.money.can_spend_at(Region.island_resort, 600), Beverage.triple_shot_espresso: self.has("Hot Java Ring"), + Consumable.butterfly_powder: self.money.can_spend_at(Region.sewer, 20000), + Consumable.far_away_stone: self.region.can_reach(Region.mines_floor_100) & self.has(Artifact.ancient_doll), + Consumable.fireworks_red: self.region.can_reach(Region.casino), + Consumable.fireworks_purple: self.region.can_reach(Region.casino), + Consumable.fireworks_green: self.region.can_reach(Region.casino), + Consumable.golden_animal_cracker: self.skill.has_mastery(Skill.farming), + Consumable.mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride), + Consumable.gold_mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride) & self.skill.has_mastery(Skill.foraging), + Currency.calico_egg: self.region.can_reach(LogicRegion.desert_festival), + Currency.golden_tag: self.region.can_reach(LogicRegion.trout_derby), + Currency.prize_ticket: self.time.has_lived_months(2), # Time to do a few help wanted quests Decoration.rotten_plant: self.has(Lighting.jack_o_lantern) & self.season.has(Season.winter), Fertilizer.basic: self.money.can_spend_at(Region.pierre_store, 100), Fertilizer.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Fertilizer.tree: self.skill.has_level(Skill.foraging, 7) & self.has(Material.fiber) & self.has(Material.stone), - Fish.any: Or(*(self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value))), + Fish.any: self.logic.or_(*(self.fishing.can_catch_fish(fish) for fish in content.fishes.values())), Fish.crab: self.skill.can_crab_pot_at(Region.beach), Fish.crayfish: self.skill.can_crab_pot_at(Region.town), Fish.lobster: self.skill.can_crab_pot_at(Region.beach), @@ -252,44 +234,15 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Fish.snail: self.skill.can_crab_pot_at(Region.town), Fishing.curiosity_lure: self.monster.can_kill(self.monster.all_monsters_by_name[Monster.mummy]), Fishing.lead_bobber: self.skill.has_level(Skill.fishing, 6) & self.money.can_spend_at(Region.fish_shop, 200), - Forageable.blackberry: self.tool.can_forage(Season.fall) | self.has_fruit_bats(), - Forageable.cactus_fruit: self.tool.can_forage(Generic.any, Region.desert), - Forageable.cave_carrot: self.tool.can_forage(Generic.any, Region.mines_floor_10, True), - Forageable.chanterelle: self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.coconut: self.tool.can_forage(Generic.any, Region.desert), - Forageable.common_mushroom: self.tool.can_forage(Season.fall) | (self.tool.can_forage(Season.spring, Region.secret_woods)) | self.has_mushroom_cave(), - Forageable.crocus: self.tool.can_forage(Season.winter), - Forageable.crystal_fruit: self.tool.can_forage(Season.winter), - Forageable.daffodil: self.tool.can_forage(Season.spring), - Forageable.dandelion: self.tool.can_forage(Season.spring), - Forageable.dragon_tooth: self.tool.can_forage(Generic.any, Region.volcano_floor_10), - Forageable.fiddlehead_fern: self.tool.can_forage(Season.summer, Region.secret_woods), - Forageable.ginger: self.tool.can_forage(Generic.any, Region.island_west, True), - Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), - Forageable.hazelnut: self.tool.can_forage(Season.fall), - Forageable.holly: self.tool.can_forage(Season.winter), - Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), - Forageable.leek: self.tool.can_forage(Season.spring), - Forageable.magma_cap: self.tool.can_forage(Generic.any, Region.volcano_floor_5), - Forageable.morel: self.tool.can_forage(Season.spring, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.purple_mushroom: self.tool.can_forage(Generic.any, Region.mines_floor_95) | self.tool.can_forage(Generic.any, Region.skull_cavern_25) | self.has_mushroom_cave(), - Forageable.rainbow_shell: self.tool.can_forage(Season.summer, Region.beach), - Forageable.red_mushroom: self.tool.can_forage(Season.summer, Region.secret_woods) | self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.salmonberry: self.tool.can_forage(Season.spring) | self.has_fruit_bats(), - Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), - Forageable.snow_yam: self.tool.can_forage(Season.winter, Region.beach, True), - Forageable.spice_berry: self.tool.can_forage(Season.summer) | self.has_fruit_bats(), - Forageable.spring_onion: self.tool.can_forage(Season.spring), - Forageable.sweet_pea: self.tool.can_forage(Season.summer), - Forageable.wild_horseradish: self.tool.can_forage(Season.spring), - Forageable.wild_plum: self.tool.can_forage(Season.fall) | self.has_fruit_bats(), - Forageable.winter_root: self.tool.can_forage(Season.winter, Region.forest, True), + Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), # + Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()),# + Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), # Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton), Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe), Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe), Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut), Fossil.fossilized_spine: self.skill.can_fish(Region.dig_site), - Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site), + Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper), Fossil.mummified_bat: self.region.can_reach(Region.volcano_floor_10), Fossil.mummified_frog: self.region.can_reach(Region.island_east) & self.tool.has_tool(Tool.scythe), Fossil.snake_skull: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.hoe), @@ -299,10 +252,10 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Geode.geode: self.mine.can_mine_in_the_mines_floor_1_40(), Geode.golden_coconut: self.region.can_reach(Region.island_north), Geode.magma: self.mine.can_mine_in_the_mines_floor_81_120() | (self.has(Fish.lava_eel) & self.building.has_building(Building.fish_pond)), - Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.action.can_pan() | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10), - Gift.bouquet: self.relationship.has_hearts(Generic.bachelor, 8) & self.money.can_spend_at(Region.pierre_store, 100), + Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10), + Gift.bouquet: self.relationship.has_hearts_with_any_bachelor(8) & self.money.can_spend_at(Region.pierre_store, 100), Gift.golden_pumpkin: self.season.has(Season.fall) | self.action.can_open_geode(Geode.artifact_trove), - Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts(Generic.bachelor, 10) & self.building.has_house(1) & self.has(Consumable.rain_totem), + Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts_with_any_bachelor(10) & self.building.has_house(1) & self.has(Consumable.rain_totem), Gift.movie_ticket: self.money.can_spend_at(Region.movie_ticket_stand, 1000), Gift.pearl: (self.has(Fish.blobfish) & self.building.has_building(Building.fish_pond)) | self.action.can_open_geode(Geode.artifact_trove), Gift.tea_set: self.season.has(Season.winter) & self.time.has_lived_max_months, @@ -312,45 +265,27 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Ingredient.qi_seasoning: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 10), Ingredient.rice: self.money.can_spend_at(Region.pierre_store, 200) | (self.building.has_building(Building.mill) & self.has(Vegetable.unmilled_rice)), Ingredient.sugar: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.beet)), - Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200), + Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200) | self.artisan.can_keg(Ingredient.rice), Ingredient.wheat_flour: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.wheat)), Loot.bat_wing: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(), Loot.bug_meat: self.mine.can_mine_in_the_mines_floor_1_40(), Loot.slime: self.mine.can_mine_in_the_mines_floor_1_40(), Loot.solar_essence: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(), Loot.void_essence: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern(), - Machine.bee_house: self.skill.has_farming_level(3) & self.has(MetalBar.iron) & self.has(ArtisanGood.maple_syrup) & self.has(Material.coal) & self.has(Material.wood), - Machine.cask: self.building.has_house(3) & self.region.can_reach(Region.cellar) & self.has(Material.wood) & self.has(Material.hardwood), - Machine.cheese_press: self.skill.has_farming_level(6) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.hardwood) & self.has(MetalBar.copper), Machine.coffee_maker: self.received(Machine.coffee_maker), - Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & (self.money.can_spend_at(Region.fish_shop, 1500) | (self.has(MetalBar.iron) & self.has(Material.wood))), - Machine.furnace: self.has(Material.stone) & self.has(Ore.copper), - Machine.keg: self.skill.has_farming_level(8) & self.has(Material.wood) & self.has(MetalBar.iron) & self.has(MetalBar.copper) & self.has(ArtisanGood.oak_resin), - Machine.lightning_rod: self.skill.has_level(Skill.foraging, 6) & self.has(MetalBar.iron) & self.has(MetalBar.quartz) & self.has(Loot.bat_wing), - Machine.loom: self.skill.has_farming_level(7) & self.has(Material.wood) & self.has(Material.fiber) & self.has(ArtisanGood.pine_tar), - Machine.mayonnaise_machine: self.skill.has_farming_level(2) & self.has(Material.wood) & self.has(Material.stone) & self.has("Earth Crystal") & self.has(MetalBar.copper), - Machine.ostrich_incubator: self.received("Ostrich Incubator Recipe") & self.has(Fossil.bone_fragment) & self.has(Material.hardwood) & self.has(Material.cinder_shard), - Machine.preserves_jar: self.skill.has_farming_level(4) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.coal), - Machine.recycling_machine: self.skill.has_level(Skill.fishing, 4) & self.has(Material.wood) & self.has(Material.stone) & self.has(MetalBar.iron), - Machine.seed_maker: self.skill.has_farming_level(9) & self.has(Material.wood) & self.has(MetalBar.gold) & self.has(Material.coal), - Machine.solar_panel: self.received("Solar Panel Recipe") & self.has(MetalBar.quartz) & self.has(MetalBar.iron) & self.has(MetalBar.gold), - Machine.tapper: self.skill.has_level(Skill.foraging, 3) & self.has(Material.wood) & self.has(MetalBar.copper), - Machine.worm_bin: self.skill.has_level(Skill.fishing, 8) & self.has(Material.hardwood) & self.has(MetalBar.gold) & self.has(MetalBar.iron) & self.has(Material.fiber), + Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & self.money.can_spend_at(Region.fish_shop, 1500), Machine.enricher: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20), Machine.pressure_nozzle: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20), Material.cinder_shard: self.region.can_reach(Region.volcano_floor_5), Material.clay: self.region.can_reach_any((Region.farm, Region.beach, Region.quarry)) & self.tool.has_tool(Tool.hoe), - Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.action.can_pan(), + Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.tool.has_tool(Tool.pan), Material.fiber: True_(), Material.hardwood: self.tool.has_tool(Tool.axe, ToolMaterial.copper) & (self.region.can_reach(Region.secret_woods) | self.region.can_reach(Region.island_west)), + Material.moss: self.season.has_any_not_winter() & (self.tool.has_tool(Tool.scythe) | self.combat.has_any_weapon) & self.region.can_reach(Region.forest), Material.sap: self.ability.can_chop_trees(), Material.stone: self.tool.has_tool(Tool.pickaxe), Material.wood: self.tool.has_tool(Tool.axe), - Meal.bread: self.money.can_spend_at(Region.saloon, 120), Meal.ice_cream: (self.season.has(Season.summer) & self.money.can_spend_at(Region.town, 250)) | self.money.can_spend_at(Region.oasis, 240), - Meal.pizza: self.money.can_spend_at(Region.saloon, 600), - Meal.salad: self.money.can_spend_at(Region.saloon, 220), - Meal.spaghetti: self.money.can_spend_at(Region.saloon, 240), Meal.strange_bun: self.relationship.has_hearts(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise), MetalBar.copper: self.can_smelt(Ore.copper), MetalBar.gold: self.can_smelt(Ore.gold), @@ -358,15 +293,14 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti MetalBar.iron: self.can_smelt(Ore.iron), MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))), MetalBar.radioactive: self.can_smelt(Ore.radioactive), - Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), - Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), - Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber), - Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), + Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), + Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron), + Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold), + Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room), RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100), RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Sapling.tea: self.relationship.has_hearts(NPC.caroline, 2) & self.has(Material.fiber) & self.has(Material.wood), - Seed.mixed: self.tool.has_tool(Tool.scythe) & self.region.can_reach_all((Region.farm, Region.forest, Region.town)), SpeedGro.basic: self.money.can_spend_at(Region.pierre_store, 100), SpeedGro.deluxe: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Trash.broken_cd: self.skill.can_crab_pot, @@ -380,24 +314,33 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti TreeSeed.maple: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(), TreeSeed.mushroom: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 5), TreeSeed.pine: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(), - Vegetable.tea_leaves: self.has(Sapling.tea) & self.time.has_lived_months(2) & self.season.has_any_not_winter(), + TreeSeed.mossy: self.ability.can_chop_trees() & self.season.has(Season.summer), Fish.clam: self.tool.can_forage(Generic.any, Region.beach), Fish.cockle: self.tool.can_forage(Generic.any, Region.beach), - WaterItem.coral: self.tool.can_forage(Generic.any, Region.tide_pools) | self.tool.can_forage(Season.summer, Region.beach), WaterItem.green_algae: self.fishing.can_fish_in_freshwater(), - WaterItem.nautilus_shell: self.tool.can_forage(Season.winter, Region.beach), - WaterItem.sea_urchin: self.tool.can_forage(Generic.any, Region.tide_pools), + WaterItem.cave_jelly: self.fishing.can_fish_at(Region.mines_floor_100) & self.tool.has_fishing_rod(2), + WaterItem.river_jelly: self.fishing.can_fish_at(Region.town) & self.tool.has_fishing_rod(2), + WaterItem.sea_jelly: self.fishing.can_fish_at(Region.beach) & self.tool.has_fishing_rod(2), WaterItem.seaweed: self.skill.can_fish(Region.tide_pools), WaterItem.white_algae: self.skill.can_fish(Region.mines_floor_20), WildSeeds.grass_starter: self.money.can_spend_at(Region.pierre_store, 100), }) # @formatter:on + + content_rules = { + item_name: self.source.has_access_to_item(game_item) + for item_name, game_item in self.content.game_items.items() + } + + for item in set(content_rules.keys()).intersection(self.registry.item_rules.keys()): + logger.warning(f"Rule for {item} already exists in the registry, overwriting it.") + + self.registry.item_rules.update(content_rules) self.registry.item_rules.update(self.registry.fish_rules) self.registry.item_rules.update(self.registry.museum_rules) - self.registry.item_rules.update(self.registry.sapling_rules) - self.registry.item_rules.update(self.registry.tree_fruit_rules) - self.registry.item_rules.update(self.registry.seed_rules) self.registry.item_rules.update(self.registry.crop_rules) + self.artisan.initialize_rules() + self.registry.item_rules.update(self.registry.artisan_good_rules) self.registry.item_rules.update(self.mod.item.get_modded_item_rules()) self.mod.item.modify_vanilla_item_rules_with_mod_additions(self.registry.item_rules) # New regions and content means new ways to obtain old items @@ -420,81 +363,20 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti 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(Generic.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(), - }) + self.festival.initialize_rules() self.special_order.initialize_rules() self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules()) - def can_buy_sapling(self, fruit: str) -> StardewRule: - sapling_prices = {Fruit.apple: 4000, Fruit.apricot: 2000, Fruit.cherry: 3400, Fruit.orange: 4000, - Fruit.peach: 6000, - Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0} - received_sapling = self.received(f"{fruit} Sapling") - if self.options.cropsanity == Cropsanity.option_disabled: - allowed_buy_sapling = True_() - else: - allowed_buy_sapling = received_sapling - can_buy_sapling = self.money.can_spend_at(Region.pierre_store, sapling_prices[fruit]) - if fruit == Fruit.banana: - can_buy_sapling = self.has_island_trader() & self.has(Forageable.dragon_tooth) - elif fruit == Fruit.mango: - can_buy_sapling = self.has_island_trader() & self.has(Fish.mussel_node) - - return allowed_buy_sapling & can_buy_sapling + def setup_events(self, register_event: Callable[[str, str, StardewRule], None]) -> None: + for logic_event in all_logic_events: + rule = self.registry.item_rules[logic_event.item] + register_event(logic_event.name, logic_event.region, rule) + self.registry.item_rules[logic_event.item] = self.received(logic_event.name) def can_smelt(self, item: str) -> StardewRule: return self.has(Machine.furnace) & self.has(item) - def can_complete_field_office(self) -> StardewRule: - field_office = self.region.can_reach(Region.field_office) - professor_snail = self.received("Open Professor Snail Cave") - tools = self.tool.has_tool(Tool.pickaxe) & self.tool.has_tool(Tool.hoe) & self.tool.has_tool(Tool.scythe) - leg_and_snake_skull = self.has_all(Fossil.fossilized_leg, Fossil.snake_skull) - ribs_and_spine = self.has_all(Fossil.fossilized_ribs, Fossil.fossilized_spine) - skull = self.has(Fossil.fossilized_skull) - tail = self.has(Fossil.fossilized_tail) - frog = self.has(Fossil.mummified_frog) - bat = self.has(Fossil.mummified_bat) - snake_vertebrae = self.has(Fossil.snake_vertebrae) - return field_office & professor_snail & tools & leg_and_snake_skull & ribs_and_spine & skull & tail & frog & bat & snake_vertebrae - def can_finish_grandpa_evaluation(self) -> StardewRule: # https://stardewvalleywiki.com/Grandpa rules_worth_a_point = [ @@ -511,9 +393,9 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: # Catching every fish not expected # Shipping every item not expected self.relationship.can_get_married() & self.building.has_house(2), - self.relationship.has_hearts("5", 8), # 5 Friends - self.relationship.has_hearts("10", 8), # 10 friends - self.pet.has_hearts(5), # Max Pet + self.relationship.has_hearts_with_n(5, 8), # 5 Friends + self.relationship.has_hearts_with_n(10, 8), # 10 friends + self.pet.has_pet_hearts(5), # Max Pet self.bundle.can_complete_community_center, # Community Center Completion self.bundle.can_complete_community_center, # CC Ceremony first point self.bundle.can_complete_community_center, # CC Ceremony second point @@ -522,97 +404,11 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: ] return self.count(12, *rules_worth_a_point) - def can_win_egg_hunt(self) -> StardewRule: - number_of_movement_buffs = self.options.movement_buff_number - if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2: - return True_() - return self.received(Buff.movement, number_of_movement_buffs // 2) - - def can_succeed_luau_soup(self) -> StardewRule: - if self.options.festival_locations != FestivalLocations.option_hard: - return True_() - eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, Fish.mutant_carp, - Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"] - fish_rule = self.has_any(*eligible_fish) - 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] - aged_rule = self.has(Machine.cask) & 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.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, ] - fruit_rule = self.has_any(*good_fruits) - good_vegetables = [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] - 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_() return self.region.can_reach(Region.island_trader) - def has_walnut(self, number: int) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - return False_() - if number <= 0: - return True_() - # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations - reach_south = self.region.can_reach(Region.island_south) - reach_north = self.region.can_reach(Region.island_north) - reach_west = self.region.can_reach(Region.island_west) - reach_hut = self.region.can_reach(Region.leo_hut) - reach_southeast = self.region.can_reach(Region.island_south_east) - reach_field_office = self.region.can_reach(Region.field_office) - reach_pirate_cove = self.region.can_reach(Region.pirate_cove) - reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut) - reach_volcano_regions = [self.region.can_reach(Region.volcano), - self.region.can_reach(Region.volcano_secret_beach), - self.region.can_reach(Region.volcano_floor_5), - self.region.can_reach(Region.volcano_floor_10)] - reach_volcano = Or(*reach_volcano_regions) - reach_all_volcano = And(*reach_volcano_regions) - reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] - reach_caves = And(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), - self.region.can_reach(Region.gourmand_frog_cave), - self.region.can_reach(Region.colored_crystals_cave), - self.region.can_reach(Region.shipwreck), self.received(APWeapon.slingshot)) - reach_entire_island = And(reach_outside_areas, reach_all_volcano, - reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) - if number <= 5: - return Or(reach_south, reach_north, reach_west, reach_volcano) - if number <= 10: - return self.count(2, *reach_walnut_regions) - if number <= 15: - return self.count(3, *reach_walnut_regions) - if number <= 20: - return And(*reach_walnut_regions) - if number <= 50: - return reach_entire_island - gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) - return reach_entire_island & self.has(Fruit.banana) & self.has_all(*gems) & self.ability.can_mine_perfectly() & \ - self.ability.can_fish_perfectly() & self.has(Furniture.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \ - self.can_complete_field_office() - def has_all_stardrops(self) -> StardewRule: other_rules = [] number_of_stardrops_to_receive = 0 @@ -621,20 +417,22 @@ def has_all_stardrops(self) -> StardewRule: number_of_stardrops_to_receive += 1 # Museum Stardrop number_of_stardrops_to_receive += 1 # Krobus Stardrop - if self.options.fishsanity == Fishsanity.option_none: # Master Angler Stardrop - other_rules.append(self.fishing.can_catch_every_fish()) - else: + # Master Angler Stardrop + if self.content.features.fishsanity.is_enabled: number_of_stardrops_to_receive += 1 + else: + other_rules.append(self.fishing.can_catch_every_fish()) if self.options.festival_locations == FestivalLocations.option_disabled: # Fair Stardrop other_rules.append(self.season.has(Season.fall)) else: number_of_stardrops_to_receive += 1 - if self.options.friendsanity == Friendsanity.option_none: # Spouse Stardrop - other_rules.append(self.relationship.has_hearts(Generic.bachelor, 13)) - else: + # Spouse Stardrop + if self.content.features.friendsanity.is_enabled: number_of_stardrops_to_receive += 1 + else: + other_rules.append(self.relationship.has_hearts_with_any_bachelor(13)) if ModNames.deepwoods in self.options.mods: # Petting the Unicorn number_of_stardrops_to_receive += 1 @@ -642,18 +440,7 @@ def has_all_stardrops(self) -> StardewRule: if not other_rules: return self.received("Stardrop", number_of_stardrops_to_receive) - return self.received("Stardrop", number_of_stardrops_to_receive) & And(*other_rules) - - def has_prismatic_jelly_reward_access(self) -> StardewRule: - if self.options.special_order_locations == SpecialOrderLocations.option_disabled: - return self.special_order.can_complete_special_order("Prismatic Jelly") - return self.received("Monster Musk Recipe") - - def has_all_rarecrows(self) -> StardewRule: - rules = [] - for rarecrow_number in range(1, 9): - rules.append(self.received(f"Rarecrow #{rarecrow_number}")) - return And(*rules) + return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules) def has_abandoned_jojamart(self) -> StardewRule: return self.received(CommunityUpgrade.movie_theater, 1) @@ -664,11 +451,5 @@ def has_movie_theater(self) -> StardewRule: def can_use_obelisk(self, obelisk: str) -> StardewRule: return self.region.can_reach(Region.farm) & self.received(obelisk) - def has_fruit_bats(self) -> StardewRule: - return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.fruit_bats) - - def has_mushroom_cave(self) -> StardewRule: - return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.mushroom_boxes) - def can_fish_pond(self, fish: str) -> StardewRule: return self.building.has_building(Building.fish_pond) & self.has(fish) diff --git a/worlds/stardew_valley/logic/logic_and_mods_design.md b/worlds/stardew_valley/logic/logic_and_mods_design.md index 14716e1af0e1..87631175b391 100644 --- a/worlds/stardew_valley/logic/logic_and_mods_design.md +++ b/worlds/stardew_valley/logic/logic_and_mods_design.md @@ -55,4 +55,21 @@ dependencies. Vanilla would always be first, then anything that depends only on 3. In `create_items`, AP items are unpacked, and randomized. 4. In `set_rules`, the rules are applied to the AP entrances and locations. Each content pack have to apply the proper rules for their entrances and locations. - (idea) To begin this step, sphere 0 could be simplified instantly as sphere 0 regions and items are already known. -5. Nothing to do in `generate_basic`. +5. Nothing to do in `generate_basic`. + +## Item Sources + +Instead of containing rules directly, items would contain sources that would then be transformed into rules. Using a single dispatch mechanism, the sources will +be associated to their actual logic. + +This system is extensible and easily maintainable in the ways that it decouple the rule and the actual items. Any "type" of item could be used with any "type" +of source (Monster drop and fish can have foraging sources). + +- Mods requiring special rules can remove sources from vanilla content or wrap them to add their own logic (Magic add sources for some items), or change the + rules for monster drop sources. +- (idea) A certain difficulty level (or maybe tags) could be added to the source, to enable or disable them given settings chosen by the player. Someone with a + high grinding tolerance can enable "hard" or "grindy" sources. Some source that are pushed back in further spheres can be replaced by less forgiving sources + if easy logic is disabled. For instance, anything that requires money could be accessible as soon as you can sell something to someone (even wood). + +Items are classified by their source. An item with a fishing or a crab pot source is considered a fish, an item dropping from a monster is a monster drop. An +item with a foraging source is a forageable. Items can fit in multiple categories. diff --git a/worlds/stardew_valley/logic/logic_event.py b/worlds/stardew_valley/logic/logic_event.py new file mode 100644 index 000000000000..9af1d622578f --- /dev/null +++ b/worlds/stardew_valley/logic/logic_event.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass + +from ..strings.ap_names import event_names +from ..strings.metal_names import MetalBar, Ore +from ..strings.region_names import Region + +all_events = event_names.all_events.copy() +all_logic_events = list() + + +@dataclass(frozen=True) +class LogicEvent: + name: str + region: str + + +@dataclass(frozen=True) +class LogicItemEvent(LogicEvent): + item: str + + def __init__(self, item: str, region: str): + super().__init__(f"{item} (Logic event)", region) + super().__setattr__("item", item) + + +def register_item_event(item: str, region: str = Region.farm): + event = LogicItemEvent(item, region) + all_logic_events.append(event) + all_events.add(event.name) + + +for i in (MetalBar.copper, MetalBar.iron, MetalBar.gold, MetalBar.iridium, Ore.copper, Ore.iron, Ore.gold, Ore.iridium): + register_item_event(i) diff --git a/worlds/stardew_valley/logic/mine_logic.py b/worlds/stardew_valley/logic/mine_logic.py index 2c2eaabfd8ee..350582ae0dbb 100644 --- a/worlds/stardew_valley/logic/mine_logic.py +++ b/worlds/stardew_valley/logic/mine_logic.py @@ -3,13 +3,15 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin +from .cooking_logic import CookingLogicMixin +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .skill_logic import SkillLogicMixin from .tool_logic import ToolLogicMixin from .. import options from ..options import ToolProgression -from ..stardew_rule import StardewRule, And, True_ +from ..stardew_rule import StardewRule, True_ from ..strings.performance_names import Performance from ..strings.region_names import Region from ..strings.skill_names import Skill @@ -22,7 +24,8 @@ def __init__(self, *args, **kwargs): self.mine = MineLogic(*args, **kwargs) -class MineLogic(BaseLogic[Union[MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin, SkillLogicMixin]]): +class MineLogic(BaseLogic[Union[HasLogicMixin, MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin, +SkillLogicMixin, CookingLogicMixin]]): # Regions def can_mine_in_the_mines_floor_1_40(self) -> StardewRule: return self.logic.region.can_reach(Region.mines_floor_5) @@ -55,13 +58,20 @@ def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: rules = [] weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier) rules.append(weapon_rule) + if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) - if self.options.skill_progression == options.SkillProgression.option_progressive: - skill_tier = min(10, max(0, tier * 2)) - rules.append(self.logic.skill.has_level(Skill.combat, skill_tier)) - rules.append(self.logic.skill.has_level(Skill.mining, skill_tier)) - return And(*rules) + + # No alternative for vanilla because we assume that you will grind the levels in the mines. + if self.content.features.skill_progression.is_progressive: + skill_level = min(10, max(0, tier * 2)) + rules.append(self.logic.skill.has_level(Skill.combat, skill_level)) + rules.append(self.logic.skill.has_level(Skill.mining, skill_level)) + + if tier >= 4: + rules.append(self.logic.cooking.can_cook()) + + return self.logic.and_(*rules) @cache_self1 def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: @@ -77,10 +87,14 @@ def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule rules = [] weapon_rule = self.logic.combat.has_great_weapon rules.append(weapon_rule) + if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) - if self.options.skill_progression == options.SkillProgression.option_progressive: - skill_tier = min(10, max(0, tier * 2 + 6)) - rules.extend({self.logic.skill.has_level(Skill.combat, skill_tier), - self.logic.skill.has_level(Skill.mining, skill_tier)}) - return And(*rules) + + # No alternative for vanilla because we assume that you will grind the levels in the mines. + if self.content.features.skill_progression.is_progressive: + skill_level = min(10, max(0, tier * 2 + 6)) + rules.extend((self.logic.skill.has_level(Skill.combat, skill_level), + self.logic.skill.has_level(Skill.mining, skill_level))) + + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/money_logic.py b/worlds/stardew_valley/logic/money_logic.py index 92945a3636a8..85370273c987 100644 --- a/worlds/stardew_valley/logic/money_logic.py +++ b/worlds/stardew_valley/logic/money_logic.py @@ -1,17 +1,24 @@ +import typing from typing import Union from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic -from .buff_logic import BuffLogicMixin +from .grind_logic import GrindLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin +from ..data.shop import ShopSource from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_ -from ..strings.ap_names.event_names import Event +from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_, true_ from ..strings.currency_names import Currency -from ..strings.region_names import Region +from ..strings.region_names import Region, LogicRegion + +if typing.TYPE_CHECKING: + from .shipping_logic import ShippingLogicMixin + + assert ShippingLogicMixin qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems", "20 Qi Gems", "15 Qi Gems", "10 Qi Gems") @@ -23,7 +30,8 @@ def __init__(self, *args, **kwargs): self.money = MoneyLogic(*args, **kwargs) -class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, BuffLogicMixin]]): +class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin, +GrindLogicMixin, 'ShippingLogicMixin']]): @cache_self1 def can_have_earned_total(self, amount: int) -> StardewRule: @@ -31,10 +39,10 @@ def can_have_earned_total(self, amount: int) -> StardewRule: return True_() pierre_rule = self.logic.region.can_reach_all((Region.pierre_store, Region.forest)) - willy_rule = self.logic.region.can_reach_all((Region.fish_shop, Region.fishing)) + willy_rule = self.logic.region.can_reach_all((Region.fish_shop, LogicRegion.fishing)) clint_rule = self.logic.region.can_reach_all((Region.blacksmith, Region.mines_floor_5)) robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods)) - shipping_rule = self.logic.received(Event.can_ship_items) + shipping_rule = self.logic.shipping.can_use_shipping_bin if amount < 2000: selling_any_rule = pierre_rule | willy_rule | clint_rule | robin_rule | shipping_rule @@ -47,7 +55,7 @@ def can_have_earned_total(self, amount: int) -> StardewRule: if amount < 10000: return shipping_rule - seed_rules = self.logic.received(Event.can_shop_at_pierre) + seed_rules = self.logic.region.can_reach(Region.pierre_store) if amount < 40000: return shipping_rule & seed_rules @@ -64,6 +72,20 @@ def can_spend(self, amount: int) -> StardewRule: def can_spend_at(self, region: str, amount: int) -> StardewRule: return self.logic.region.can_reach(region) & self.logic.money.can_spend(amount) + @cache_self1 + def can_shop_from(self, source: ShopSource) -> StardewRule: + season_rule = self.logic.season.has_any(source.seasons) + money_rule = self.logic.money.can_spend(source.money_price) if source.money_price is not None else true_ + + item_rules = [] + if source.items_price is not None: + for price, item in source.items_price: + item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price)) + + region_rule = self.logic.region.can_reach(source.shop_region) + + return self.logic.and_(season_rule, money_rule, *item_rules, region_rule) + # Should be cached def can_trade(self, currency: str, amount: int) -> StardewRule: if amount == 0: @@ -71,11 +93,11 @@ def can_trade(self, currency: str, amount: int) -> StardewRule: if currency == Currency.money: return self.can_spend(amount) if currency == Currency.star_token: - return self.logic.region.can_reach(Region.fair) + return self.logic.region.can_reach(LogicRegion.fair) if currency == Currency.qi_coin: - return self.logic.region.can_reach(Region.casino) & self.logic.buff.has_max_luck() + return self.logic.region.can_reach(Region.casino) & self.logic.time.has_lived_months(amount // 1000) if currency == Currency.qi_gem: - if self.options.special_order_locations == SpecialOrderLocations.option_board_qi: + if self.options.special_order_locations & SpecialOrderLocations.value_qi: number_rewards = min(len(qi_gem_rewards), max(1, (amount // 10))) return self.logic.received_n(*qi_gem_rewards, count=number_rewards) number_rewards = 2 @@ -84,7 +106,7 @@ def can_trade(self, currency: str, amount: int) -> StardewRule: if currency == Currency.golden_walnut: return self.can_spend_walnut(amount) - return self.logic.has(currency) & self.logic.time.has_lived_months(amount) + return self.logic.has(currency) & self.logic.grind.can_grind_item(amount) # Should be cached def can_trade_at(self, region: str, currency: str, amount: int) -> StardewRule: diff --git a/worlds/stardew_valley/logic/monster_logic.py b/worlds/stardew_valley/logic/monster_logic.py index 790f492347e6..7e6d786972ac 100644 --- a/worlds/stardew_valley/logic/monster_logic.py +++ b/worlds/stardew_valley/logic/monster_logic.py @@ -4,11 +4,13 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin, MAX_MONTHS from .. import options from ..data import monster_data -from ..stardew_rule import StardewRule, Or, And +from ..stardew_rule import StardewRule +from ..strings.generic_names import Generic from ..strings.region_names import Region @@ -18,7 +20,7 @@ def __init__(self, *args, **kwargs): self.monster = MonsterLogic(*args, **kwargs) -class MonsterLogic(BaseLogic[Union[MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]): +class MonsterLogic(BaseLogic[Union[HasLogicMixin, MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]): @cached_property def all_monsters_by_name(self): @@ -29,13 +31,18 @@ def all_monsters_by_category(self): return monster_data.all_monsters_by_category_given_mods(self.options.mods.value) def can_kill(self, monster: Union[str, monster_data.StardewMonster], amount_tier: int = 0) -> StardewRule: + if amount_tier <= 0: + amount_tier = 0 + time_rule = self.logic.time.has_lived_months(amount_tier) + if isinstance(monster, str): + if monster == Generic.any: + return self.logic.monster.can_kill_any(self.all_monsters_by_name.values()) & time_rule + monster = self.all_monsters_by_name[monster] region_rule = self.logic.region.can_reach_any(monster.locations) combat_rule = self.logic.combat.can_fight_at_level(monster.difficulty) - if amount_tier <= 0: - amount_tier = 0 - time_rule = self.logic.time.has_lived_months(amount_tier) + return region_rule & combat_rule & time_rule @cache_self1 @@ -48,13 +55,11 @@ def can_kill_max(self, monster: monster_data.StardewMonster) -> StardewRule: # Should be cached def can_kill_any(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule: - rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters] - return Or(*rules) + return self.logic.or_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters)) # Should be cached def can_kill_all(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule: - rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters] - return And(*rules) + return self.logic.and_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters)) def can_complete_all_monster_slaying_goals(self) -> StardewRule: rules = [self.logic.time.has_lived_max_months] @@ -66,4 +71,4 @@ def can_complete_all_monster_slaying_goals(self) -> StardewRule: continue rules.append(self.logic.monster.can_kill_any(self.all_monsters_by_category[category])) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 59ef0f6499c1..36ba62b31fcb 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -6,10 +6,14 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin from .. import options from ..data.museum_data import MuseumItem, all_museum_items, all_museum_artifacts, all_museum_minerals -from ..stardew_rule import StardewRule, And, False_ +from ..stardew_rule import StardewRule, False_ +from ..strings.metal_names import Mineral from ..strings.region_names import Region +from ..strings.tool_names import Tool, ToolMaterial class MuseumLogicMixin(BaseLogicMixin): @@ -18,7 +22,7 @@ def __init__(self, *args, **kwargs): self.museum = MuseumLogic(*args, **kwargs) -class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, ActionLogicMixin, MuseumLogicMixin]]): +class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, ActionLogicMixin, ToolLogicMixin, MuseumLogicMixin]]): def can_donate_museum_items(self, number: int) -> StardewRule: return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_items(number) @@ -33,15 +37,16 @@ def can_find_museum_item(self, item: MuseumItem) -> StardewRule: else: region_rule = False_() if item.geodes: - geodes_rule = And(*(self.logic.action.can_open_geode(geode) for geode in item.geodes)) + geodes_rule = self.logic.and_(*(self.logic.action.can_open_geode(geode) for geode in item.geodes)) else: geodes_rule = False_() # monster_rule = self.can_farm_monster(item.monsters) - # extra_rule = True_() + time_needed_to_grind = int((20 - item.difficulty) // 2) + time_rule = self.logic.time.has_lived_months(time_needed_to_grind) pan_rule = False_() - if item.item_name == "Earth Crystal" or item.item_name == "Fire Quartz" or item.item_name == "Frozen Tear": - pan_rule = self.logic.action.can_pan() - return pan_rule | region_rule | geodes_rule # & monster_rule & extra_rule + if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear: + pan_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) + return (pan_rule | region_rule | geodes_rule) & time_rule # & monster_rule & extra_rule def can_find_museum_artifacts(self, number: int) -> StardewRule: rules = [] @@ -74,7 +79,7 @@ def can_complete_museum(self) -> StardewRule: for donation in all_museum_items: rules.append(self.logic.museum.can_find_museum_item(donation)) - return And(*rules) & self.logic.region.can_reach(Region.museum) + return self.logic.and_(*rules) & self.logic.region.can_reach(Region.museum) def can_donate(self, item: str) -> StardewRule: return self.logic.has(item) & self.logic.region.can_reach(Region.museum) diff --git a/worlds/stardew_valley/logic/pet_logic.py b/worlds/stardew_valley/logic/pet_logic.py index 5d7d79a358ca..0438940a6633 100644 --- a/worlds/stardew_valley/logic/pet_logic.py +++ b/worlds/stardew_valley/logic/pet_logic.py @@ -6,11 +6,9 @@ from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from ..data.villagers_data import Villager -from ..options import Friendsanity +from ..content.feature.friendsanity import pet_heart_item_name from ..stardew_rule import StardewRule, True_ from ..strings.region_names import Region -from ..strings.villager_names import NPC class PetLogicMixin(BaseLogicMixin): @@ -20,21 +18,25 @@ def __init__(self, *args, **kwargs): class PetLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, TimeLogicMixin, ToolLogicMixin]]): - def has_hearts(self, hearts: int = 1) -> StardewRule: - if hearts <= 0: + def has_pet_hearts(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, "You can't have negative hearts with a pet." + if hearts == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none or self.options.friendsanity == Friendsanity.option_bachelors: - return self.can_befriend_pet(hearts) - return self.received_hearts(NPC.pet, hearts) - def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule: - if isinstance(npc, Villager): - return self.received_hearts(npc.name, hearts) - return self.logic.received(self.heart(npc), math.ceil(hearts / self.options.friendsanity_heart_size)) + if self.content.features.friendsanity.is_pet_randomized: + return self.received_pet_hearts(hearts) + + return self.can_befriend_pet(hearts) + + def received_pet_hearts(self, hearts: int) -> StardewRule: + return self.logic.received(pet_heart_item_name, + math.ceil(hearts / self.content.features.friendsanity.heart_size)) def can_befriend_pet(self, hearts: int) -> StardewRule: - if hearts <= 0: + assert hearts >= 0, "You can't have negative hearts with a pet." + if hearts == 0: return True_() + points = hearts * 200 points_per_month = 12 * 14 points_per_water_month = 18 * 14 @@ -43,8 +45,3 @@ def can_befriend_pet(self, hearts: int) -> StardewRule: time_without_water_rule = self.logic.time.has_lived_months(points // points_per_month) time_rule = time_with_water_rule | time_without_water_rule return farm_rule & time_rule - - def heart(self, npc: Union[str, Villager]) -> str: - if isinstance(npc, str): - return f"{npc} <3" - return self.heart(npc.name) diff --git a/worlds/stardew_valley/logic/quality_logic.py b/worlds/stardew_valley/logic/quality_logic.py new file mode 100644 index 000000000000..54e2d242654b --- /dev/null +++ b/worlds/stardew_valley/logic/quality_logic.py @@ -0,0 +1,33 @@ +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .farming_logic import FarmingLogicMixin +from .skill_logic import SkillLogicMixin +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.quality_names import CropQuality + + +class QualityLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.quality = QualityLogic(*args, **kwargs) + + +class QualityLogic(BaseLogic[Union[SkillLogicMixin, FarmingLogicMixin]]): + + @cache_self1 + def can_grow_crop_quality(self, quality: str) -> StardewRule: + if quality == CropQuality.basic: + return True_() + if quality == CropQuality.silver: + return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | ( + self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3) + if quality == CropQuality.gold: + return self.logic.skill.has_farming_level(10) | ( + self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | ( + self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | ( + self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2)) + if quality == CropQuality.iridium: + return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4) + return False_() diff --git a/worlds/stardew_valley/logic/quest_logic.py b/worlds/stardew_valley/logic/quest_logic.py index bc1f731429c6..42f401b96025 100644 --- a/worlds/stardew_valley/logic/quest_logic.py +++ b/worlds/stardew_valley/logic/quest_logic.py @@ -17,6 +17,7 @@ from .tool_logic import ToolLogicMixin from .wallet_logic import WalletLogicMixin from ..stardew_rule import StardewRule, Has, True_ +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building from ..strings.craftable_names import Craftable @@ -43,7 +44,8 @@ def __init__(self, *args, **kwargs): class QuestLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, MoneyLogicMixin, MineLogicMixin, RegionLogicMixin, RelationshipLogicMixin, ToolLogicMixin, -FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin, BuildingLogicMixin, TimeLogicMixin]]): + FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin, + BuildingLogicMixin, TimeLogicMixin]]): def initialize_rules(self): self.update_rules({ @@ -52,6 +54,7 @@ def initialize_rules(self): Quest.getting_started: self.logic.has(Vegetable.parsnip), Quest.to_the_beach: self.logic.region.can_reach(Region.beach), Quest.raising_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.coop), + Quest.feeding_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.silo), Quest.advancement: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.has(Craftable.scarecrow), Quest.archaeology: self.logic.tool.has_tool(Tool.hoe) | self.logic.mine.can_mine_in_the_mines_floor_1_40() | self.logic.skill.can_fish(), Quest.rat_problem: self.logic.region.can_reach_all((Region.town, Region.community_center)), @@ -63,7 +66,8 @@ def initialize_rules(self): Quest.jodis_request: self.logic.season.has(Season.spring) & self.logic.has(Vegetable.cauliflower) & self.logic.relationship.can_meet(NPC.jodi), Quest.mayors_shorts: self.logic.season.has(Season.summer) & self.logic.relationship.has_hearts(NPC.marnie, 2) & self.logic.relationship.can_meet(NPC.lewis), - Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus), + Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus) & self.logic.region.can_reach( + Region.tunnel_entrance), Quest.marnies_request: self.logic.relationship.has_hearts(NPC.marnie, 3) & self.logic.has(Forageable.cave_carrot), Quest.pam_is_thirsty: self.logic.season.has(Season.summer) & self.logic.has(ArtisanGood.pale_ale) & self.logic.relationship.can_meet(NPC.pam), Quest.a_dark_reagent: self.logic.season.has(Season.winter) & self.logic.has(Loot.void_essence) & self.logic.relationship.can_meet(NPC.wizard), @@ -104,13 +108,14 @@ def initialize_rules(self): Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) & self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) & self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy), + Quest.giant_stump: self.logic.has(Material.hardwood) }) def update_rules(self, new_rules: Dict[str, StardewRule]): self.registry.quest_rules.update(new_rules) def can_complete_quest(self, quest: str) -> StardewRule: - return Has(quest, self.registry.quest_rules) + return Has(quest, self.registry.quest_rules, "quest") def has_club_card(self) -> StardewRule: if self.options.quest_locations < 0: @@ -126,3 +131,12 @@ def has_dark_talisman(self) -> StardewRule: if self.options.quest_locations < 0: return self.logic.quest.can_complete_quest(Quest.dark_talisman) return self.logic.received(Wallet.dark_talisman) + + def has_raccoon_shop(self) -> StardewRule: + if self.options.quest_locations < 0: + return self.logic.received(CommunityUpgrade.raccoon, 2) & self.logic.quest.can_complete_quest(Quest.giant_stump) + + # 1 - Break the tree + # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off + # 3 - Raccoon's wife opens the shop + return self.logic.received(CommunityUpgrade.raccoon, 3) diff --git a/worlds/stardew_valley/logic/received_logic.py b/worlds/stardew_valley/logic/received_logic.py index 66dc078ad46f..f5c5c9f7a206 100644 --- a/worlds/stardew_valley/logic/received_logic.py +++ b/worlds/stardew_valley/logic/received_logic.py @@ -1,26 +1,32 @@ from typing import Optional +from BaseClasses import ItemClassification from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin -from ..stardew_rule import StardewRule, Received, And, Or, TotalReceived +from .logic_event import all_events +from ..items import item_table +from ..stardew_rule import StardewRule, Received, TotalReceived class ReceivedLogicMixin(BaseLogic[HasLogicMixin], BaseLogicMixin): - # Should be cached def received(self, item: str, count: Optional[int] = 1) -> StardewRule: assert count >= 0, "Can't receive a negative amount of item." + if item in all_events: + return Received(item, self.player, count, event=True) + + assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic" return Received(item, self.player, count) def received_all(self, *items: str): assert items, "Can't receive all of no items." - return And(*(self.received(item) for item in items)) + return self.logic.and_(*(self.received(item) for item in items)) def received_any(self, *items: str): assert items, "Can't receive any of no items." - return Or(*(self.received(item) for item in items)) + return self.logic.or_(*(self.received(item) for item in items)) def received_once(self, *items: str, count: int): assert items, "Can't receive once of no items." @@ -32,4 +38,7 @@ def received_n(self, *items: str, count: int): assert items, "Can't receive n of no items." assert count >= 0, "Can't receive a negative amount of item." + for item in items: + assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic" + return TotalReceived(count, items, self.player) diff --git a/worlds/stardew_valley/logic/region_logic.py b/worlds/stardew_valley/logic/region_logic.py index 81dabf45aac5..69afa624f22c 100644 --- a/worlds/stardew_valley/logic/region_logic.py +++ b/worlds/stardew_valley/logic/region_logic.py @@ -4,7 +4,7 @@ from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin from ..options import EntranceRandomization -from ..stardew_rule import StardewRule, And, Or, Reach, false_, true_ +from ..stardew_rule import StardewRule, Reach, false_, true_ from ..strings.region_names import Region main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest, @@ -18,6 +18,7 @@ always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er, EntranceRandomization.option_pelican_town: always_accessible_regions_without_er, EntranceRandomization.option_non_progression: always_accessible_regions_without_er, + EntranceRandomization.option_buildings_without_house: main_outside_area, EntranceRandomization.option_buildings: main_outside_area, EntranceRandomization.option_chaos: always_accessible_regions_without_er} @@ -42,11 +43,14 @@ def can_reach(self, region_name: str) -> StardewRule: @cache_self1 def can_reach_any(self, region_names: Tuple[str, ...]) -> StardewRule: - return Or(*(self.logic.region.can_reach(spot) for spot in region_names)) + if any(r in always_regions_by_setting[self.options.entrance_randomization] for r in region_names): + return true_ + + return self.logic.or_(*(self.logic.region.can_reach(spot) for spot in region_names)) @cache_self1 def can_reach_all(self, region_names: Tuple[str, ...]) -> StardewRule: - return And(*(self.logic.region.can_reach(spot) for spot in region_names)) + return self.logic.and_(*(self.logic.region.can_reach(spot) for spot in region_names)) @cache_self1 def can_reach_all_except_one(self, region_names: Tuple[str, ...]) -> StardewRule: diff --git a/worlds/stardew_valley/logic/relationship_logic.py b/worlds/stardew_valley/logic/relationship_logic.py index fb0267bddb1a..61e63a90c83a 100644 --- a/worlds/stardew_valley/logic/relationship_logic.py +++ b/worlds/stardew_valley/logic/relationship_logic.py @@ -1,6 +1,5 @@ import math -from functools import cached_property -from typing import Union, List +from typing import Union from Utils import cache_self1 from .base_logic import BaseLogic, BaseLogicMixin @@ -11,9 +10,9 @@ from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin -from ..data.villagers_data import all_villagers_by_name, Villager, get_villagers_for_mods -from ..options import Friendsanity -from ..stardew_rule import StardewRule, True_, And, Or +from ..content.feature import friendsanity +from ..data.villagers_data import Villager +from ..stardew_rule import StardewRule, True_, false_, true_ from ..strings.ap_names.mods.mod_items import SVEQuestItem from ..strings.crop_names import Fruit from ..strings.generic_names import Generic @@ -38,12 +37,8 @@ def __init__(self, *args, **kwargs): self.relationship = RelationshipLogic(*args, **kwargs) -class RelationshipLogic(BaseLogic[Union[ - RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]): - - @cached_property - def all_villagers_given_mods(self) -> List[Villager]: - return get_villagers_for_mods(self.options.mods.value) +class RelationshipLogic(BaseLogic[Union[RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin, +ReceivedLogicMixin, HasLogicMixin]]): def can_date(self, npc: str) -> StardewRule: return self.logic.relationship.has_hearts(npc, 8) & self.logic.has(Gift.bouquet) @@ -52,134 +47,160 @@ def can_marry(self, npc: str) -> StardewRule: return self.logic.relationship.has_hearts(npc, 10) & self.logic.has(Gift.mermaid_pendant) def can_get_married(self) -> StardewRule: - return self.logic.relationship.has_hearts(Generic.bachelor, 10) & self.logic.has(Gift.mermaid_pendant) + return self.logic.relationship.has_hearts_with_any_bachelor(10) & self.logic.has(Gift.mermaid_pendant) def has_children(self, number_children: int) -> StardewRule: - if number_children <= 0: + assert number_children >= 0, "Can't have a negative amount of children." + if number_children == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none: + + if not self.content.features.friendsanity.is_enabled: return self.logic.relationship.can_reproduce(number_children) + return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_house(2) def can_reproduce(self, number_children: int = 1) -> StardewRule: - if number_children <= 0: + assert number_children >= 0, "Can't have a negative amount of children." + if number_children == 0: return True_() - baby_rules = [self.logic.relationship.can_get_married(), self.logic.building.has_house(2), self.logic.relationship.has_hearts(Generic.bachelor, 12), + + baby_rules = [self.logic.relationship.can_get_married(), + self.logic.building.has_house(2), + self.logic.relationship.has_hearts_with_any_bachelor(12), self.logic.relationship.has_children(number_children - 1)] - return And(*baby_rules) - # Should be cached - def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule: - if hearts <= 0: + return self.logic.and_(*baby_rules) + + @cache_self1 + def has_hearts_with_any_bachelor(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any bachelor." + if hearts == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none: - return self.logic.relationship.can_earn_relationship(npc, hearts) - if npc not in all_villagers_by_name: - if npc == Generic.any or npc == Generic.bachelor: - possible_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - if npc == Generic.any or all_villagers_by_name[name].bachelor: - possible_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return Or(*possible_friends) - if npc == Generic.all: - mandatory_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - mandatory_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return And(*mandatory_friends) - if npc.isnumeric(): - possible_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - possible_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return self.logic.count(int(npc), *possible_friends) - return self.can_earn_relationship(npc, hearts) - - if not self.npc_is_in_current_slot(npc): + + return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items() + if villager.bachelor)) + + @cache_self1 + def has_hearts_with_any(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any npc." + if hearts == 0: return True_() - villager = all_villagers_by_name[npc] - if self.options.friendsanity == Friendsanity.option_bachelors and not villager.bachelor: - return self.logic.relationship.can_earn_relationship(npc, hearts) - if self.options.friendsanity == Friendsanity.option_starting_npcs and not villager.available: + + return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items())) + + def has_hearts_with_n(self, amount: int, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any npc." + assert amount >= 0, f"Can't have a negative amount of npc." + if hearts == 0 or amount == 0: + return True_() + + return self.logic.count(amount, *(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items())) + + # Should be cached + def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with {npc}." + + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + + if hearts == 0: + return true_ + + heart_steps = self.content.features.friendsanity.get_randomized_hearts(villager) + if not heart_steps or hearts > heart_steps[-1]: # Hearts are sorted, bigger is the last one. return self.logic.relationship.can_earn_relationship(npc, hearts) - is_capped_at_8 = villager.bachelor and self.options.friendsanity != Friendsanity.option_all_with_marriage - if is_capped_at_8 and hearts > 8: - return self.logic.relationship.received_hearts(villager.name, 8) & self.logic.relationship.can_earn_relationship(npc, hearts) - return self.logic.relationship.received_hearts(villager.name, hearts) + + return self.logic.relationship.received_hearts(villager, hearts) # Should be cached - def received_hearts(self, npc: str, hearts: int) -> StardewRule: - heart_item = heart_item_name(npc) - number_required = math.ceil(hearts / self.options.friendsanity_heart_size) - return self.logic.received(heart_item, number_required) + def received_hearts(self, villager: Villager, hearts: int) -> StardewRule: + heart_item = friendsanity.to_item_name(villager.name) + + number_required = math.ceil(hearts / self.content.features.friendsanity.heart_size) + return self.logic.received(heart_item, number_required) & self.can_meet(villager.name) @cache_self1 def can_meet(self, npc: str) -> StardewRule: - if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc): - return True_() - villager = all_villagers_by_name[npc] + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + rules = [self.logic.region.can_reach_any(villager.locations)] + if npc == NPC.kent: rules.append(self.logic.time.has_year_two) + elif npc == NPC.leo: - rules.append(self.logic.received("Island West Turtle")) + rules.append(self.logic.received("Island North Turtle")) + elif npc == ModNPC.lance: rules.append(self.logic.region.can_reach(Region.volcano_floor_10)) + elif npc == ModNPC.apples: rules.append(self.logic.has(Fruit.starfruit)) + elif npc == ModNPC.scarlett: scarlett_job = self.logic.received(SVEQuestItem.scarlett_job_offer) scarlett_spring = self.logic.season.has(Season.spring) & self.can_meet(ModNPC.andy) scarlett_summer = self.logic.season.has(Season.summer) & self.can_meet(ModNPC.susan) scarlett_fall = self.logic.season.has(Season.fall) & self.can_meet(ModNPC.sophia) rules.append(scarlett_job & (scarlett_spring | scarlett_summer | scarlett_fall)) + elif npc == ModNPC.morgan: rules.append(self.logic.received(SVEQuestItem.morgan_schooling)) + elif npc == ModNPC.goblin: rules.append(self.logic.region.can_reach_all((Region.witch_hut, Region.wizard_tower))) - return And(*rules) + return self.logic.and_(*rules) def can_give_loved_gifts_to_everyone(self) -> StardewRule: rules = [] - for npc in all_villagers_by_name: - if not self.npc_is_in_current_slot(npc): - continue + + for npc in self.content.villagers: meet_rule = self.logic.relationship.can_meet(npc) rules.append(meet_rule) + rules.append(self.logic.gifts.has_any_universal_love) - return And(*rules) + + return self.logic.and_(*rules) # Should be cached def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: - if hearts <= 0: + assert hearts >= 0, f"Can't have a negative hearts with {npc}." + + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + + if hearts == 0: return True_() - previous_heart = hearts - self.options.friendsanity_heart_size - previous_heart_rule = self.logic.relationship.has_hearts(npc, previous_heart) + rules = [self.logic.relationship.can_meet(npc)] - if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc): - return previous_heart_rule + heart_size = self.content.features.friendsanity.heart_size + max_randomized_hearts = self.content.features.friendsanity.get_randomized_hearts(villager) + if max_randomized_hearts: + if hearts > max_randomized_hearts[-1]: + rules.append(self.logic.relationship.has_hearts(npc, hearts - 1)) + else: + previous_heart = max(hearts - heart_size, 0) + rules.append(self.logic.relationship.has_hearts(npc, previous_heart)) - rules = [previous_heart_rule, self.logic.relationship.can_meet(npc)] - villager = all_villagers_by_name[npc] - if hearts > 2 or hearts > self.options.friendsanity_heart_size: + if hearts > 2 or hearts > heart_size: rules.append(self.logic.season.has(villager.birthday)) + if villager.birthday == Generic.any: rules.append(self.logic.season.has_all() | self.logic.time.has_year_three) # push logic back for any birthday-less villager + if villager.bachelor: - if hearts > 8: - rules.append(self.logic.relationship.can_date(npc)) if hearts > 10: rules.append(self.logic.relationship.can_marry(npc)) + elif hearts > 8: + rules.append(self.logic.relationship.can_date(npc)) - return And(*rules) - - @cache_self1 - def npc_is_in_current_slot(self, name: str) -> bool: - npc = all_villagers_by_name[name] - return npc in self.all_villagers_given_mods + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py new file mode 100644 index 000000000000..6a5adf4890c9 --- /dev/null +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -0,0 +1,80 @@ +import functools +from typing import Union, Iterable + +from .base_logic import BaseLogicMixin, BaseLogic +from .book_logic import BookLogicMixin +from .combat_logic import CombatLogicMixin +from .fishing_logic import FishingLogicMixin +from .has_logic import HasLogicMixin +from .quest_logic import QuestLogicMixin +from .received_logic import ReceivedLogicMixin +from .relationship_logic import RelationshipLogicMixin +from .season_logic import SeasonLogicMixin +from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin +from .walnut_logic import WalnutLogicMixin +from ..data.game_item import Requirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \ + RelationshipRequirement, FishingRequirement, WalnutRequirement + + +class RequirementLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.requirement = RequirementLogic(*args, **kwargs) + + +class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, +SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]): + + def meet_all_requirements(self, requirements: Iterable[Requirement]): + if not requirements: + return self.logic.true_ + return self.logic.and_(*(self.logic.requirement.meet_requirement(requirement) for requirement in requirements)) + + @functools.singledispatchmethod + def meet_requirement(self, requirement: Requirement): + raise ValueError(f"Requirements of type{type(requirement)} have no rule registered.") + + @meet_requirement.register + def _(self, requirement: ToolRequirement): + return self.logic.tool.has_tool(requirement.tool, requirement.tier) + + @meet_requirement.register + def _(self, requirement: SkillRequirement): + return self.logic.skill.has_level(requirement.skill, requirement.level) + + @meet_requirement.register + def _(self, requirement: BookRequirement): + return self.logic.book.has_book_power(requirement.book) + + @meet_requirement.register + def _(self, requirement: SeasonRequirement): + return self.logic.season.has(requirement.season) + + @meet_requirement.register + def _(self, requirement: YearRequirement): + return self.logic.time.has_year(requirement.year) + + @meet_requirement.register + def _(self, requirement: WalnutRequirement): + return self.logic.walnut.has_walnut(requirement.amount) + + @meet_requirement.register + def _(self, requirement: CombatRequirement): + return self.logic.combat.can_fight_at_level(requirement.level) + + @meet_requirement.register + def _(self, requirement: QuestRequirement): + return self.logic.quest.can_complete_quest(requirement.quest) + + @meet_requirement.register + def _(self, requirement: RelationshipRequirement): + return self.logic.relationship.has_hearts(requirement.npc, requirement.hearts) + + @meet_requirement.register + def _(self, requirement: FishingRequirement): + return self.logic.fishing.can_fish_at(requirement.region) + + diff --git a/worlds/stardew_valley/logic/season_logic.py b/worlds/stardew_valley/logic/season_logic.py index 1953502099b4..6df315c0db94 100644 --- a/worlds/stardew_valley/logic/season_logic.py +++ b/worlds/stardew_valley/logic/season_logic.py @@ -1,11 +1,13 @@ +from functools import cached_property from typing import Iterable, Union from Utils import cache_self1 from .base_logic import BaseLogic, BaseLogicMixin +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .time_logic import TimeLogicMixin from ..options import SeasonRandomization -from ..stardew_rule import StardewRule, True_, Or, And +from ..stardew_rule import StardewRule, True_, true_ from ..strings.generic_names import Generic from ..strings.season_names import Season @@ -16,7 +18,23 @@ def __init__(self, *args, **kwargs): self.season = SeasonLogic(*args, **kwargs) -class SeasonLogic(BaseLogic[Union[SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]): +class SeasonLogic(BaseLogic[Union[HasLogicMixin, SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]): + + @cached_property + def has_spring(self) -> StardewRule: + return self.logic.season.has(Season.spring) + + @cached_property + def has_summer(self) -> StardewRule: + return self.logic.season.has(Season.summer) + + @cached_property + def has_fall(self) -> StardewRule: + return self.logic.season.has(Season.fall) + + @cached_property + def has_winter(self) -> StardewRule: + return self.logic.season.has(Season.winter) @cache_self1 def has(self, season: str) -> StardewRule: @@ -32,13 +50,16 @@ def has(self, season: str) -> StardewRule: return self.logic.received(season) def has_any(self, seasons: Iterable[str]): + if seasons == Season.all: + return true_ if not seasons: + # That should be false, but I'm scared. return True_() - return Or(*(self.logic.season.has(season) for season in seasons)) + return self.logic.or_(*(self.logic.season.has(season) for season in seasons)) def has_any_not_winter(self): return self.logic.season.has_any([Season.spring, Season.summer, Season.fall]) def has_all(self): seasons = [Season.spring, Season.summer, Season.fall, Season.winter] - return And(*(self.logic.season.has(season) for season in seasons)) + return self.logic.and_(*(self.logic.season.has(season) for season in seasons)) diff --git a/worlds/stardew_valley/logic/shipping_logic.py b/worlds/stardew_valley/logic/shipping_logic.py index 52c97561b326..e9f2258172e6 100644 --- a/worlds/stardew_valley/logic/shipping_logic.py +++ b/worlds/stardew_valley/logic/shipping_logic.py @@ -10,8 +10,7 @@ from ..locations import LocationTags, locations_by_tag from ..options import ExcludeGingerIsland, Shipsanity from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, And -from ..strings.ap_names.event_names import Event +from ..stardew_rule import StardewRule from ..strings.building_names import Building @@ -29,13 +28,13 @@ def can_use_shipping_bin(self) -> StardewRule: @cache_self1 def can_ship(self, item: str) -> StardewRule: - return self.logic.received(Event.can_ship_items) & self.logic.has(item) + return self.logic.shipping.can_use_shipping_bin & self.logic.has(item) def can_ship_everything(self) -> StardewRule: shipsanity_prefix = "Shipsanity: " all_items_to_ship = [] exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_qi = self.options.special_order_locations != SpecialOrderLocations.option_board_qi + exclude_qi = not (self.options.special_order_locations & SpecialOrderLocations.value_qi) mod_list = self.options.mods.value for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]: if exclude_island and LocationTags.GINGER_ISLAND in location.tags: @@ -49,7 +48,7 @@ def can_ship_everything(self) -> StardewRule: def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule: if self.options.shipsanity == Shipsanity.option_none: - return self.can_ship_everything() + return self.logic.shipping.can_ship_everything() rules = [self.logic.building.has_building(Building.shipping_bin)] @@ -57,4 +56,4 @@ def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> if shipsanity_location.name not in all_location_names_in_slot: continue rules.append(self.logic.region.can_reach_location(shipsanity_location.name)) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 35946a0a4d36..bc2f6cb1263d 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -4,27 +4,28 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin -from .crop_logic import CropLogicMixin +from .harvesting_logic import HarvestingLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from .. import options -from ..data import all_crops +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_, Or, False_ +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 fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west) +vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level") class SkillLogicMixin(BaseLogicMixin): @@ -34,28 +35,25 @@ def __init__(self, *args, **kwargs): class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin, -CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): +CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): + # Should be cached 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, 1)) + xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) elif skill == Skill.farming: - xp_rule = self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) + 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.logic.tool.has_tool(Tool.axe, tool_material) | self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) + 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) | \ self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) @@ -66,22 +64,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 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_progressive: + if self.content.features.skill_progression.is_progressive: return self.logic.received(f"{skill} Level", level) return self.logic.skill.can_earn_level(skill, 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.content.features.skill_progression.is_progressive: + return self.logic.received(f"{skill} Level", level - 1) + + months = max(1, level - 1) + return self.logic.time.has_lived_months(months) + @cache_self1 def has_farming_level(self, level: int) -> StardewRule: return self.logic.skill.has_level(Skill.farming, level) @@ -91,8 +101,8 @@ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> Star if level <= 0: return True_() - if self.options.skill_progression == options.SkillProgression.option_progressive: - skills_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level") + if self.content.features.skill_progression.is_progressive: + skills_items = vanilla_skill_items if allow_modded_skills: skills_items += get_mod_skill_levels(self.options.mods) return self.logic.received_n(*skills_items, count=level) @@ -104,12 +114,17 @@ 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_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: + sources = self.content.find_sources_of_type(HarvestCropSource) crop_rules = [] - for crop in all_crops: - crop_rules.append(self.logic.crop.can_grow(crop)) - return Or(*crop_rules) + for crop_source in sources: + crop_rules.append(self.logic.harvesting.can_harvest_crop_from(crop_source)) + return self.logic.or_(*crop_rules) @cached_property def can_get_foraging_xp(self) -> StardewRule: @@ -132,7 +147,7 @@ def can_get_combat_xp(self) -> StardewRule: @cached_property def can_get_fishing_xp(self) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.content.features.skill_progression.is_progressive: return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot return self.logic.skill.can_fish() @@ -162,7 +177,9 @@ def can_crab_pot_at(self, region: str) -> StardewRule: @cached_property def can_crab_pot(self) -> StardewRule: crab_pot_rule = self.logic.has(Fishing.bait) - if self.options.skill_progression == options.SkillProgression.option_progressive: + + # We can't use the same rule if skills are vanilla, because fishing levels are required to crab pot, which is required to get fishing levels... + if self.content.features.skill_progression.is_progressive: crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot) else: crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp @@ -178,3 +195,20 @@ def can_forage_quality(self, quality: str) -> StardewRule: if quality == ForageQuality.gold: return self.has_level(Skill.foraging, 9) return False_() + + 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.content.features.skill_progression.are_masteries_shuffled: + 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.content.features.skill_progression.are_masteries_shuffled: + 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 new file mode 100644 index 000000000000..9ef68a020eef --- /dev/null +++ b/worlds/stardew_valley/logic/source_logic.py @@ -0,0 +1,114 @@ +import functools +from typing import Union, Any, Iterable + +from .artisan_logic import ArtisanLogicMixin +from .base_logic import BaseLogicMixin, BaseLogic +from .grind_logic import GrindLogicMixin +from .harvesting_logic import HarvestingLogicMixin +from .has_logic import HasLogicMixin +from .money_logic import MoneyLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .requirement_logic import RequirementLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.artisan import MachineSource +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 + + +class SourceLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.source = SourceLogic(*args, **kwargs) + + +class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin, + ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): + + def has_access_to_item(self, item: GameItem): + rules = [] + + if self.content.features.cropsanity.is_included(item): + rules.append(self.logic.received(item.name)) + + rules.append(self.logic.source.has_access_to_any(item.sources)) + return self.logic.and_(*rules) + + 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.") + + @has_access_to.register + def _(self, source: GenericSource): + return self.logic.region.can_reach_any(source.regions) if source.regions else self.logic.true_ + + @has_access_to.register + 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) + + @has_access_to.register + def _(self, source: SeasonalForagingSource): + # Implementation could be different with some kind of "calendar shuffle" + return self.logic.harvesting.can_forage_from(source.as_foraging_source()) + + @has_access_to.register + def _(self, _: FruitBatsSource): + return self.logic.harvesting.can_harvest_from_fruit_bats + + @has_access_to.register + def _(self, _: MushroomCaveSource): + return self.logic.harvesting.can_harvest_from_mushroom_cave + + @has_access_to.register + def _(self, source: ShopSource): + return self.logic.money.can_shop_from(source) + + @has_access_to.register + def _(self, source: HarvestFruitTreeSource): + return self.logic.harvesting.can_harvest_tree_from(source) + + @has_access_to.register + def _(self, source: HarvestCropSource): + return self.logic.harvesting.can_harvest_crop_from(source) + + @has_access_to.register + def _(self, source: MachineSource): + return self.logic.artisan.can_produce_from(source) + + @has_access_to.register + def _(self, source: MysteryBoxSource): + return self.logic.grind.can_grind_mystery_boxes(source.amount) + + @has_access_to.register + def _(self, source: ArtifactTroveSource): + return self.logic.grind.can_grind_artifact_troves(source.amount) + + @has_access_to.register + def _(self, source: PrizeMachineSource): + return self.logic.grind.can_grind_prize_tickets(source.amount) + + @has_access_to.register + def _(self, source: FishingTreasureChestSource): + return self.logic.grind.can_grind_fishing_treasure_chests(source.amount) + + @has_access_to.register + def _(self, source: ArtifactSpotSource): + return self.logic.grind.can_grind_artifact_spots(source.amount) diff --git a/worlds/stardew_valley/logic/special_order_logic.py b/worlds/stardew_valley/logic/special_order_logic.py index e0b1a7e2fb27..8bcd78d7d26e 100644 --- a/worlds/stardew_valley/logic/special_order_logic.py +++ b/worlds/stardew_valley/logic/special_order_logic.py @@ -4,7 +4,6 @@ from .arcade_logic import ArcadeLogicMixin from .artisan_logic import ArtisanLogicMixin from .base_logic import BaseLogicMixin, BaseLogic -from .buff_logic import BuffLogicMixin from .cooking_logic import CookingLogicMixin from .has_logic import HasLogicMixin from .mine_logic import MineLogicMixin @@ -18,9 +17,10 @@ from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from ..stardew_rule import StardewRule, Has +from ..content.vanilla.ginger_island import ginger_island_content_pack +from ..content.vanilla.qi_board import qi_board_content_pack +from ..stardew_rule import StardewRule, Has, false_ from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.event_names import Event from ..strings.ap_names.transport_names import Transportation from ..strings.artisan_good_names import ArtisanGood from ..strings.crop_names import Vegetable, Fruit @@ -35,7 +35,6 @@ from ..strings.region_names import Region from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder -from ..strings.tool_names import Tool from ..strings.villager_names import NPC @@ -47,14 +46,11 @@ def __init__(self, *args, **kwargs): class SpecialOrderLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, MoneyLogicMixin, ShippingLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, RelationshipLogicMixin, ToolLogicMixin, SkillLogicMixin, -MineLogicMixin, CookingLogicMixin, BuffLogicMixin, +MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]): def initialize_rules(self): self.update_rules({ - SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() & - self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) & - self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger), SpecialOrder.cave_patrol: self.logic.relationship.can_meet(NPC.clint), SpecialOrder.aquatic_overpopulation: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(), SpecialOrder.biome_balance: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(), @@ -64,48 +60,65 @@ def initialize_rules(self): SpecialOrder.gifts_for_george: self.logic.season.has(Season.spring) & self.logic.has(Forageable.leek), SpecialOrder.fragments_of_the_past: self.logic.monster.can_kill(Monster.skeleton), SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg), - SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.received(Event.can_ship_items), + SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_use_shipping_bin, SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot, - SpecialOrder.the_strong_stuff: self.logic.artisan.can_keg(Vegetable.potato), + SpecialOrder.the_strong_stuff: self.logic.has(ArtisanGood.specific_juice(Vegetable.potato)), SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(), SpecialOrder.robins_project: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() & self.logic.has(Material.hardwood), SpecialOrder.robins_resource_rush: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() & self.logic.has(Fertilizer.tree) & self.logic.ability.can_mine_perfectly(), SpecialOrder.juicy_bugs_wanted: self.logic.has(Loot.bug_meat), - SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") & - self.logic.special_order.has_island_transport() & - self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish), SpecialOrder.a_curious_substance: self.logic.region.can_reach(Region.wizard_tower), SpecialOrder.prismatic_jelly: self.logic.region.can_reach(Region.wizard_tower), - SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) & - self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) & - self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items), - SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(), - SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") & - self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), - SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(), - SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) & - (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)), - SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(), - SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) & - self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend), - SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120), - SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(), - SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat - self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti - self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore - self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay - self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads - self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola - self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive... + }) + if ginger_island_content_pack.name in self.content.registered_packs: + self.update_rules({ + SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() & + self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) & + self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger), + SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") & + self.logic.special_order.has_island_transport() & + self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish), + }) + else: + self.update_rules({ + SpecialOrder.island_ingredients: false_, + SpecialOrder.tropical_fish: false_, + }) + + if qi_board_content_pack.name in self.content.registered_packs: + self.update_rules({ + SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) & + self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) & + self.logic.has(Machine.seed_maker) & self.logic.shipping.can_use_shipping_bin, + SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(), + SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") & + self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.shipping.can_use_shipping_bin & + (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)), + SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(), + SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) & + self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend), + SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120), + SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat + self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti + self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore + self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay + self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads + self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola + self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive...) + }) + def update_rules(self, new_rules: Dict[str, StardewRule]): self.registry.special_order_rules.update(new_rules) def can_complete_special_order(self, special_order: str) -> StardewRule: - return Has(special_order, self.registry.special_order_rules) + return Has(special_order, self.registry.special_order_rules, "special order") def has_island_transport(self) -> StardewRule: return self.logic.received(Transportation.island_obelisk) | self.logic.received(Transportation.boat_repair) diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py index 9dcebfe82a4f..2ba76579ff45 100644 --- a/worlds/stardew_valley/logic/time_logic.py +++ b/worlds/stardew_valley/logic/time_logic.py @@ -1,38 +1,54 @@ -from functools import cached_property -from typing import Union - -from Utils import cache_self1 -from .base_logic import BaseLogic, BaseLogicMixin -from .received_logic import ReceivedLogicMixin -from ..stardew_rule import StardewRule, HasProgressionPercent, True_ - -MAX_MONTHS = 12 -MONTH_COEFFICIENT = 24 // MAX_MONTHS - - -class TimeLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.time = TimeLogic(*args, **kwargs) - - -class TimeLogic(BaseLogic[Union[TimeLogicMixin, ReceivedLogicMixin]]): - - @cache_self1 - def has_lived_months(self, number: int) -> StardewRule: - if number <= 0: - return True_() - number = min(number, MAX_MONTHS) - return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) - - @cached_property - def has_lived_max_months(self) -> StardewRule: - return self.logic.time.has_lived_months(MAX_MONTHS) - - @cached_property - def has_year_two(self) -> StardewRule: - return self.logic.time.has_lived_months(4) - - @cached_property - def has_year_three(self) -> StardewRule: - return self.logic.time.has_lived_months(8) +from functools import cached_property +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogic, BaseLogicMixin +from .has_logic import HasLogicMixin +from ..stardew_rule import StardewRule, HasProgressionPercent + +ONE_YEAR = 4 +MAX_MONTHS = 3 * ONE_YEAR +PERCENT_REQUIRED_FOR_MAX_MONTHS = 48 +MONTH_COEFFICIENT = PERCENT_REQUIRED_FOR_MAX_MONTHS // MAX_MONTHS + +MIN_ITEMS = 10 +MAX_ITEMS = 999 +PERCENT_REQUIRED_FOR_MAX_ITEM = 24 + + +class TimeLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.time = TimeLogic(*args, **kwargs) + + +class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]): + + @cache_self1 + def has_lived_months(self, number: int) -> StardewRule: + assert isinstance(number, int), "Can't have lived a fraction of a month. Use // instead of / when dividing." + if number <= 0: + return self.logic.true_ + + number = min(number, MAX_MONTHS) + return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) + + @cached_property + def has_lived_max_months(self) -> StardewRule: + return self.logic.time.has_lived_months(MAX_MONTHS) + + @cache_self1 + def has_lived_year(self, number: int) -> StardewRule: + return self.logic.time.has_lived_months(number * ONE_YEAR) + + @cache_self1 + def has_year(self, number: int) -> StardewRule: + return self.logic.time.has_lived_year(number - 1) + + @cached_property + def has_year_two(self) -> StardewRule: + return self.logic.time.has_year(2) + + @cached_property + def has_year_three(self) -> StardewRule: + return self.logic.time.has_year(3) diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index 1b1dc2a52120..ba593c085ae4 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -1,4 +1,4 @@ -from typing import Union, Iterable +from typing import Union, Iterable, Tuple from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic @@ -42,9 +42,17 @@ def __init__(self, *args, **kwargs): class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]): + + def has_all_tools(self, tools: Iterable[Tuple[str, str]]): + return self.logic.and_(*(self.logic.tool.has_tool(tool, material) for tool, material in tools)) + # Should be cached def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule: - assert tool != Tool.fishing_rod, "Use `has_fishing_rod` instead of `has_tool`." + if tool == Tool.fishing_rod: + return self.logic.tool.has_fishing_rod(tool_materials[material]) + + if tool == Tool.pan and material == ToolMaterial.basic: + material = ToolMaterial.copper # The first Pan is the copper one, so the basic one does not exist if material == ToolMaterial.basic or tool == Tool.scythe: return True_() @@ -52,7 +60,14 @@ def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule if self.options.tool_progression & ToolProgression.option_progressive: return self.logic.received(f"Progressive {tool}", tool_materials[material]) - return self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) + can_upgrade_rule = self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) + if tool == Tool.pan: + has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) + if material == ToolMaterial.copper: + return has_base_pan + return has_base_pan & can_upgrade_rule + + return can_upgrade_rule def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule: return self.has_tool(tool, material) & self.logic.region.can_reach(region) diff --git a/worlds/stardew_valley/logic/walnut_logic.py b/worlds/stardew_valley/logic/walnut_logic.py new file mode 100644 index 000000000000..4ab3b46f70d9 --- /dev/null +++ b/worlds/stardew_valley/logic/walnut_logic.py @@ -0,0 +1,135 @@ +from functools import cached_property +from typing import Union + +from .ability_logic import AbilityLogicMixin +from .base_logic import BaseLogic, BaseLogicMixin +from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from ..options import ExcludeGingerIsland, Walnutsanity +from ..stardew_rule import StardewRule, False_, True_ +from ..strings.ap_names.ap_option_names import WalnutsanityOptionName +from ..strings.ap_names.event_names import Event +from ..strings.craftable_names import Furniture +from ..strings.crop_names import Fruit +from ..strings.metal_names import Mineral, Fossil +from ..strings.region_names import Region +from ..strings.seed_names import Seed + + +class WalnutLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.walnut = WalnutLogic(*args, **kwargs) + + +class WalnutLogic(BaseLogic[Union[WalnutLogicMixin, ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, CombatLogicMixin, +AbilityLogicMixin]]): + + def has_walnut(self, number: int) -> StardewRule: + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: + return False_() + if number <= 0: + return True_() + + if self.options.walnutsanity == Walnutsanity.preset_none: + return self.can_get_walnuts(number) + if self.options.walnutsanity == Walnutsanity.preset_all: + return self.has_received_walnuts(number) + puzzle_walnuts = 61 + bush_walnuts = 25 + dig_walnuts = 18 + repeatable_walnuts = 33 + total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts + walnuts_to_receive = 0 + walnuts_to_collect = number + if WalnutsanityOptionName.puzzles in self.options.walnutsanity: + puzzle_walnut_rate = puzzle_walnuts / total_walnuts + puzzle_walnuts_required = round(puzzle_walnut_rate * number) + walnuts_to_receive += puzzle_walnuts_required + walnuts_to_collect -= puzzle_walnuts_required + if WalnutsanityOptionName.bushes in self.options.walnutsanity: + bush_walnuts_rate = bush_walnuts / total_walnuts + bush_walnuts_required = round(bush_walnuts_rate * number) + walnuts_to_receive += bush_walnuts_required + walnuts_to_collect -= bush_walnuts_required + if WalnutsanityOptionName.dig_spots in self.options.walnutsanity: + dig_walnuts_rate = dig_walnuts / total_walnuts + dig_walnuts_required = round(dig_walnuts_rate * number) + walnuts_to_receive += dig_walnuts_required + walnuts_to_collect -= dig_walnuts_required + if WalnutsanityOptionName.repeatables in self.options.walnutsanity: + repeatable_walnuts_rate = repeatable_walnuts / total_walnuts + repeatable_walnuts_required = round(repeatable_walnuts_rate * number) + walnuts_to_receive += repeatable_walnuts_required + walnuts_to_collect -= repeatable_walnuts_required + return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) + + def has_received_walnuts(self, number: int) -> StardewRule: + return self.logic.received(Event.received_walnuts, number) + + def can_get_walnuts(self, number: int) -> StardewRule: + # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations + reach_south = self.logic.region.can_reach(Region.island_south) + reach_north = self.logic.region.can_reach(Region.island_north) + reach_west = self.logic.region.can_reach(Region.island_west) + reach_hut = self.logic.region.can_reach(Region.leo_hut) + reach_southeast = self.logic.region.can_reach(Region.island_south_east) + reach_field_office = self.logic.region.can_reach(Region.field_office) + reach_pirate_cove = self.logic.region.can_reach(Region.pirate_cove) + reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) + reach_volcano_regions = [self.logic.region.can_reach(Region.volcano), + self.logic.region.can_reach(Region.volcano_secret_beach), + self.logic.region.can_reach(Region.volcano_floor_5), + self.logic.region.can_reach(Region.volcano_floor_10)] + reach_volcano = self.logic.or_(*reach_volcano_regions) + reach_all_volcano = self.logic.and_(*reach_volcano_regions) + reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] + reach_caves = self.logic.and_(self.logic.region.can_reach(Region.qi_walnut_room), self.logic.region.can_reach(Region.dig_site), + self.logic.region.can_reach(Region.gourmand_frog_cave), + self.logic.region.can_reach(Region.colored_crystals_cave), + self.logic.region.can_reach(Region.shipwreck), self.logic.combat.has_slingshot) + reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, + reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) + if number <= 5: + return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) + if number <= 10: + return self.logic.count(2, *reach_walnut_regions) + if number <= 15: + return self.logic.count(3, *reach_walnut_regions) + if number <= 20: + return self.logic.and_(*reach_walnut_regions) + if number <= 50: + return reach_entire_island + gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) + return reach_entire_island & self.logic.has(Fruit.banana) & self.logic.has_all(*gems) & \ + self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \ + self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \ + self.logic.has(Seed.garlic) & self.can_complete_field_office() + + @cached_property + def can_start_field_office(self) -> StardewRule: + field_office = self.logic.region.can_reach(Region.field_office) + professor_snail = self.logic.received("Open Professor Snail Cave") + return field_office & professor_snail + + def can_complete_large_animal_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) + return self.can_start_field_office & fossils + + def can_complete_snake_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) + return self.can_start_field_office & fossils + + def can_complete_frog_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_frog) + return self.can_start_field_office & fossils + + def can_complete_bat_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_bat) + return self.can_start_field_office & fossils + + def can_complete_field_office(self) -> StardewRule: + return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ + self.can_complete_frog_collection() & self.can_complete_bat_collection() diff --git a/worlds/stardew_valley/mods/logic/deepwoods_logic.py b/worlds/stardew_valley/mods/logic/deepwoods_logic.py index 7699521542a7..6e0eadfd5486 100644 --- a/worlds/stardew_valley/mods/logic/deepwoods_logic.py +++ b/worlds/stardew_valley/mods/logic/deepwoods_logic.py @@ -1,6 +1,5 @@ from typing import Union -from ... import options from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.combat_logic import CombatLogicMixin from ...logic.cooking_logic import CookingLogicMixin @@ -10,13 +9,13 @@ from ...logic.tool_logic import ToolLogicMixin from ...mods.mod_data import ModNames from ...options import ElevatorProgression -from ...stardew_rule import StardewRule, True_, And, true_ -from ...strings.ap_names.mods.mod_items import DeepWoodsItem, SkillLevel +from ...stardew_rule import StardewRule, True_, true_ +from ...strings.ap_names.mods.mod_items import DeepWoodsItem from ...strings.ap_names.transport_names import ModTransportation from ...strings.craftable_names import Bomb from ...strings.food_names import Meal from ...strings.performance_names import Performance -from ...strings.skill_names import Skill +from ...strings.skill_names import Skill, ModSkill from ...strings.tool_names import Tool, ToolMaterial @@ -45,11 +44,11 @@ def can_reach_woods_depth(self, depth: int) -> StardewRule: self.logic.received(ModTransportation.woods_obelisk)) tier = int(depth / 25) + 1 - if self.options.skill_progression == options.SkillProgression.option_progressive: - combat_tier = min(10, max(0, tier + 5)) - rules.append(self.logic.skill.has_level(Skill.combat, combat_tier)) + if self.content.features.skill_progression.is_progressive: + combat_level = min(10, max(0, tier + 5)) + rules.append(self.logic.skill.has_level(Skill.combat, combat_level)) - return And(*rules) + return self.logic.and_(*rules) def has_woods_rune_to_depth(self, floor: int) -> StardewRule: if self.options.elevator_progression == ElevatorProgression.option_vanilla: @@ -66,8 +65,8 @@ def can_pull_sword(self) -> StardewRule: self.logic.received(DeepWoodsItem.pendant_elder), self.logic.skill.has_total_level(40)] if ModNames.luck_skill in self.options.mods: - rules.append(self.logic.received(SkillLevel.luck, 7)) + rules.append(self.logic.skill.has_level(ModSkill.luck, 7)) else: rules.append( self.logic.has(Meal.magic_rock_candy)) # You need more luck than this, but it'll push the logic down a ways; you can get the rest there. - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index 8f5e676d8c2d..ef5eab0134d1 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -7,7 +7,7 @@ from ...logic.combat_logic import CombatLogicMixin from ...logic.cooking_logic import CookingLogicMixin from ...logic.crafting_logic import CraftingLogicMixin -from ...logic.crop_logic import CropLogicMixin +from ...logic.farming_logic import FarmingLogicMixin from ...logic.fishing_logic import FishingLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.money_logic import MoneyLogicMixin @@ -23,25 +23,15 @@ from ...options import Cropsanity from ...stardew_rule import StardewRule, True_ from ...strings.artisan_good_names import ModArtisanGood -from ...strings.craftable_names import ModCraftable, ModEdible, ModMachine -from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop, Fruit -from ...strings.fish_names import WaterItem -from ...strings.flower_names import Flower -from ...strings.food_names import SVEMeal, SVEBeverage -from ...strings.forageable_names import SVEForage, DistantLandsForageable, Forageable -from ...strings.gift_names import SVEGift +from ...strings.craftable_names import ModCraftable, ModMachine +from ...strings.fish_names import ModTrash from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil -from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.monster_drop_names import Loot from ...strings.performance_names import Performance -from ...strings.quest_names import ModQuest -from ...strings.region_names import Region, SVERegion, DeepWoodsRegion, BoardingHouseRegion -from ...strings.season_names import Season -from ...strings.seed_names import SVESeed, DistantLandsSeed -from ...strings.skill_names import Skill +from ...strings.region_names import SVERegion, DeepWoodsRegion, BoardingHouseRegion from ...strings.tool_names import Tool, ToolMaterial -from ...strings.villager_names import ModNPC display_types = [ModCraftable.wooden_display, ModCraftable.hardwood_display] display_items = all_artifacts + all_fossils @@ -53,17 +43,12 @@ def __init__(self, *args, **kwargs): self.item = ModItemLogic(*args, **kwargs) -class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, CropLogicMixin, CookingLogicMixin, FishingLogicMixin, HasLogicMixin, MoneyLogicMixin, -RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, ToolLogicMixin, CraftingLogicMixin, SkillLogicMixin, TimeLogicMixin, QuestLogicMixin]]): +class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, CookingLogicMixin, FishingLogicMixin, HasLogicMixin, MoneyLogicMixin, +RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, ToolLogicMixin, CraftingLogicMixin, SkillLogicMixin, TimeLogicMixin, QuestLogicMixin, +FarmingLogicMixin]]): def get_modded_item_rules(self) -> Dict[str, StardewRule]: items = dict() - if ModNames.sve in self.options.mods: - items.update(self.get_sve_item_rules()) - if ModNames.archaeology in self.options.mods: - items.update(self.get_archaeology_item_rules()) - if ModNames.distant_lands in self.options.mods: - items.update(self.get_distant_lands_item_rules()) if ModNames.boarding_house in self.options.mods: items.update(self.get_boarding_house_item_rules()) return items @@ -75,109 +60,22 @@ def modify_vanilla_item_rules_with_mod_additions(self, item_rule: Dict[str, Star item_rule.update(self.get_modified_item_rules_for_deep_woods(item_rule)) return item_rule - def get_sve_item_rules(self): - return {SVEGift.aged_blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 28000), - SVEGift.blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 3000), - SVESeed.fungus_seed: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - ModLoot.green_mushroom: self.logic.region.can_reach(SVERegion.highlands_outside) & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.logic.season.has_any_not_winter(), - SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk_seed), - SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus_seed), - SVEForage.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron), - SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime_seed), - SVESeed.slime_seed: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVESeed.stalk_seed: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVEForage.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void_seed), - SVESeed.void_seed: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - SVEForage.void_soul: self.logic.region.can_reach( - SVERegion.crimson_badlands) & self.logic.combat.has_good_weapon & self.logic.cooking.can_cook(), - SVEForage.winter_star_rose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.winter), - SVEForage.bearberrys: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), - SVEForage.poison_mushroom: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has_any([Season.summer, Season.fall]), - SVEForage.red_baneberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.summer), - SVEForage.ferngill_primrose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.spring), - SVEForage.goldenrod: self.logic.region.can_reach(SVERegion.summit) & ( - self.logic.season.has(Season.summer) | self.logic.season.has(Season.fall)), - SVESeed.shrub_seed: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEFruit.salal_berry: self.logic.crop.can_plant_and_grow_item([Season.spring, Season.summer]) & self.logic.has(SVESeed.shrub_seed), - ModEdible.aegis_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 28000), - ModEdible.lightning_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 12000), - ModEdible.barbarian_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 22000), - ModEdible.gravity_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 4000), - SVESeed.ancient_ferns_seed: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEVegetable.ancient_fiber: self.logic.crop.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_ferns_seed), - SVEForage.big_conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), - SVEForage.dewdrop_berry: self.logic.region.can_reach(SVERegion.enchanted_grove), - SVEForage.dried_sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & - self.logic.season.has_any([Season.summer, Season.fall])), - SVEForage.golden_ocean_flower: self.logic.region.can_reach(SVERegion.fable_reef), - SVEMeal.grampleton_orange_chicken: self.logic.money.can_spend_at(Region.saloon, 650) & self.logic.relationship.has_hearts(ModNPC.sophia, 6), - ModEdible.hero_elixir: self.logic.money.can_spend_at(SVERegion.isaac_shop, 8000), - SVEForage.lucky_four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & - self.logic.season.has_any([Season.spring, Season.summer]), - SVEForage.mushroom_colony: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west)) & - self.logic.season.has(Season.fall), - SVEForage.rusty_blade: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEForage.smelly_rafflesia: self.logic.region.can_reach(Region.secret_woods), - SVEBeverage.sports_drink: self.logic.money.can_spend_at(Region.hospital, 750), - "Stamina Capsule": self.logic.money.can_spend_at(Region.hospital, 4000), - SVEForage.thistle: self.logic.region.can_reach(SVERegion.summit), - SVEForage.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - ModLoot.void_shard: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_galaxy_weapon & - self.logic.skill.has_level(Skill.combat, 10) & self.logic.region.can_reach(Region.saloon) & self.logic.time.has_year_three - } - # @formatter:on - def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): return { Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach( SVERegion.crimson_badlands), Loot.solar_essence: items[Loot.solar_essence] | self.logic.region.can_reach(SVERegion.crimson_badlands), - Flower.tulip: items[Flower.tulip] | self.logic.tool.can_forage(Season.spring, SVERegion.sprite_spring), - Flower.blue_jazz: items[Flower.blue_jazz] | self.logic.tool.can_forage(Season.spring, SVERegion.sprite_spring), - Flower.summer_spangle: items[Flower.summer_spangle] | self.logic.tool.can_forage(Season.summer, SVERegion.sprite_spring), - Flower.sunflower: items[Flower.sunflower] | self.logic.tool.can_forage((Season.summer, Season.fall), SVERegion.sprite_spring), - Flower.fairy_rose: items[Flower.fairy_rose] | self.logic.tool.can_forage(Season.fall, SVERegion.sprite_spring), - Fruit.ancient_fruit: items[Fruit.ancient_fruit] | ( - self.logic.tool.can_forage((Season.spring, Season.summer, Season.fall), SVERegion.sprite_spring) & - self.logic.time.has_year_three) | self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Fruit.sweet_gem_berry: items[Fruit.sweet_gem_berry] | ( - self.logic.tool.can_forage((Season.spring, Season.summer, Season.fall), SVERegion.sprite_spring) & - self.logic.time.has_year_three), - WaterItem.coral: items[WaterItem.coral] | self.logic.region.can_reach(SVERegion.fable_reef), - Forageable.rainbow_shell: items[Forageable.rainbow_shell] | self.logic.region.can_reach(SVERegion.fable_reef), - WaterItem.sea_urchin: items[WaterItem.sea_urchin] | self.logic.region.can_reach(SVERegion.fable_reef), - Forageable.red_mushroom: items[Forageable.red_mushroom] | self.logic.tool.can_forage((Season.summer, Season.fall), SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Forageable.purple_mushroom: items[Forageable.purple_mushroom] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Forageable.morel: items[Forageable.morel] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west), - Forageable.chanterelle: items[Forageable.chanterelle] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), Ore.copper: items[Ore.copper] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) & self.logic.combat.can_fight_at_level(Performance.great)), Ore.iron: items[Ore.iron] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) & self.logic.combat.can_fight_at_level(Performance.great)), Ore.iridium: items[Ore.iridium] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.crimson_badlands) & self.logic.combat.can_fight_at_level(Performance.maximum)), + } def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): options_to_update = { - Fruit.apple: items[Fruit.apple] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), # Deep enough to have seen such a tree at least once - Fruit.apricot: items[Fruit.apricot] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.cherry: items[Fruit.cherry] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.orange: items[Fruit.orange] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.peach: items[Fruit.peach] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.pomegranate: items[Fruit.pomegranate] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.mango: items[Fruit.mango] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Flower.tulip: items[Flower.tulip] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.blue_jazz: items[Flower.blue_jazz] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Flower.summer_spangle: items[Flower.summer_spangle] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.poppy: items[Flower.poppy] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.fairy_rose: items[Flower.fairy_rose] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), Material.hardwood: items[Material.hardwood] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iron, DeepWoodsRegion.floor_10), Ingredient.sugar: items[Ingredient.sugar] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.gold, DeepWoodsRegion.floor_50), # Gingerbread House @@ -192,35 +90,6 @@ def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): return options_to_update - def get_archaeology_item_rules(self): - archaeology_item_rules = {} - preservation_chamber_rule = self.logic.has(ModMachine.preservation_chamber) - hardwood_preservation_chamber_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) - for item in display_items: - for display_type in display_types: - if item == "Trilobite": - location_name = f"{display_type}: Trilobite Fossil" - else: - location_name = f"{display_type}: {item}" - display_item_rule = self.logic.crafting.can_craft(all_crafting_recipes_by_name[display_type]) & self.logic.has(item) - if "Wooden" in display_type: - archaeology_item_rules[location_name] = display_item_rule & preservation_chamber_rule - else: - archaeology_item_rules[location_name] = display_item_rule & hardwood_preservation_chamber_rule - return archaeology_item_rules - - def get_distant_lands_item_rules(self): - return { - DistantLandsForageable.swamp_herb: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsForageable.brown_amanita: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsSeed.vile_ancient_fruit: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsSeed.void_mint: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsCrop.void_mint: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.void_mint), - DistantLandsCrop.vile_ancient_fruit: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.vile_ancient_fruit), - } - def get_boarding_house_item_rules(self): return { # Mob Drops from lost valley enemies @@ -282,8 +151,3 @@ def get_boarding_house_item_rules(self): BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level( Performance.great), } - - def has_seed_unlocked(self, seed_name: str): - if self.options.cropsanity == Cropsanity.option_disabled: - return True_() - return self.logic.received(seed_name) diff --git a/worlds/stardew_valley/mods/logic/mod_skills_levels.py b/worlds/stardew_valley/mods/logic/mod_skills_levels.py index 18402283857b..32b3368a8c8b 100644 --- a/worlds/stardew_valley/mods/logic/mod_skills_levels.py +++ b/worlds/stardew_valley/mods/logic/mod_skills_levels.py @@ -2,20 +2,21 @@ from ...mods.mod_data import ModNames from ...options import Mods +from ...strings.ap_names.mods.mod_items import SkillLevel def get_mod_skill_levels(mods: Mods) -> Tuple[str]: skills_items = [] if ModNames.luck_skill in mods: - skills_items.append("Luck Level") + skills_items.append(SkillLevel.luck) if ModNames.socializing_skill in mods: - skills_items.append("Socializing Level") + skills_items.append(SkillLevel.socializing) if ModNames.magic in mods: - skills_items.append("Magic Level") + skills_items.append(SkillLevel.magic) if ModNames.archaeology in mods: - skills_items.append("Archaeology Level") + skills_items.append(SkillLevel.archaeology) if ModNames.binning_skill in mods: - skills_items.append("Binning Level") + skills_items.append(SkillLevel.binning) if ModNames.cooking_skill in mods: - skills_items.append("Cooking Level") + skills_items.append(SkillLevel.cooking) return tuple(skills_items) diff --git a/worlds/stardew_valley/mods/logic/quests_logic.py b/worlds/stardew_valley/mods/logic/quests_logic.py index 40b5545ee39f..1aa71404ae51 100644 --- a/worlds/stardew_valley/mods/logic/quests_logic.py +++ b/worlds/stardew_valley/mods/logic/quests_logic.py @@ -19,7 +19,7 @@ from ...strings.forageable_names import SVEForage from ...strings.material_names import Material from ...strings.metal_names import Ore, MetalBar -from ...strings.monster_drop_names import Loot +from ...strings.monster_drop_names import Loot, ModLoot from ...strings.monster_names import Monster from ...strings.quest_names import Quest, ModQuest from ...strings.region_names import Region, SVERegion, BoardingHouseRegion @@ -86,7 +86,7 @@ def _get_sve_quest_rules(self): self.logic.relationship.can_meet(ModNPC.lance) & self.logic.region.can_reach(SVERegion.guild_summit), ModQuest.AuroraVineyard: self.logic.has(Fruit.starfruit) & self.logic.region.can_reach(SVERegion.aurora_vineyard), ModQuest.MonsterCrops: self.logic.has_all(*(SVEVegetable.monster_mushroom, SVEFruit.slime_berry, SVEFruit.monster_fruit, SVEVegetable.void_root)), - ModQuest.VoidSoul: self.logic.has(SVEForage.void_soul) & self.logic.region.can_reach(Region.farm) & + ModQuest.VoidSoul: self.logic.has(ModLoot.void_soul) & self.logic.region.can_reach(Region.farm) & self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.badlands_entrance) & self.logic.relationship.has_hearts(NPC.krobus, 10) & self.logic.quest.can_complete_quest(ModQuest.MonsterCrops) & self.logic.monster.can_kill_any((Monster.shadow_brute, Monster.shadow_shaman, Monster.shadow_sniper)), diff --git a/worlds/stardew_valley/mods/logic/skills_logic.py b/worlds/stardew_valley/mods/logic/skills_logic.py index ce8bebbffef5..ba9d27741807 100644 --- a/worlds/stardew_valley/mods/logic/skills_logic.py +++ b/worlds/stardew_valley/mods/logic/skills_logic.py @@ -1,11 +1,11 @@ from typing import Union from .magic_logic import MagicLogicMixin -from ...data.villagers_data import all_villagers from ...logic.action_logic import ActionLogicMixin from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.building_logic import BuildingLogicMixin from ...logic.cooking_logic import CookingLogicMixin +from ...logic.crafting_logic import CraftingLogicMixin from ...logic.fishing_logic import FishingLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.received_logic import ReceivedLogicMixin @@ -13,11 +13,9 @@ from ...logic.relationship_logic import RelationshipLogicMixin from ...logic.tool_logic import ToolLogicMixin from ...mods.mod_data import ModNames -from ...options import SkillProgression -from ...stardew_rule import StardewRule, False_, True_ -from ...strings.ap_names.mods.mod_items import SkillLevel -from ...strings.craftable_names import ModCraftable, ModMachine +from ...stardew_rule import StardewRule, False_, True_, And from ...strings.building_names import Building +from ...strings.craftable_names import ModCraftable, ModMachine from ...strings.geode_names import Geode from ...strings.machine_names import Machine from ...strings.region_names import Region @@ -33,12 +31,12 @@ def __init__(self, *args, **kwargs): class ModSkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, ActionLogicMixin, RelationshipLogicMixin, BuildingLogicMixin, -ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, MagicLogicMixin]]): +ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, CraftingLogicMixin, MagicLogicMixin]]): def has_mod_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() - if self.options.skill_progression == SkillProgression.option_progressive: + if self.content.features.skill_progression.is_progressive: return self.logic.received(f"{skill} Level", level) return self.can_earn_mod_skill_level(skill, level) @@ -77,24 +75,27 @@ def can_earn_magic_skill_level(self, level: int) -> StardewRule: def can_earn_socializing_skill_level(self, level: int) -> StardewRule: villager_count = [] - for villager in all_villagers: - if villager.mod_name in self.options.mods or villager.mod_name is None: - villager_count.append(self.logic.relationship.can_earn_relationship(villager.name, level)) + + for villager in self.content.villagers.values(): + villager_count.append(self.logic.relationship.can_earn_relationship(villager.name, level)) + return self.logic.count(level * 2, *villager_count) def can_earn_archaeology_skill_level(self, level: int) -> StardewRule: shifter_rule = True_() preservation_rule = True_() - if self.options.skill_progression == self.options.skill_progression.option_progressive: + if self.content.features.skill_progression.is_progressive: shifter_rule = self.logic.has(ModCraftable.water_shifter) preservation_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) if level >= 8: - return (self.logic.action.can_pan() & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule + tool_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold) + return tool_rule & shifter_rule & preservation_rule if level >= 5: - return (self.logic.action.can_pan() & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule + tool_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron) + return tool_rule & shifter_rule if level >= 3: - return self.logic.action.can_pan() | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) - return self.logic.action.can_pan() | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) + return self.logic.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) + return self.logic.tool.has_tool(Tool.pan, ToolMaterial.copper) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) def can_earn_cooking_skill_level(self, level: int) -> StardewRule: if level >= 6: @@ -104,7 +105,13 @@ def can_earn_cooking_skill_level(self, level: int) -> StardewRule: return self.logic.cooking.can_cook() def can_earn_binning_skill_level(self, level: int) -> StardewRule: - if level >= 6: - return self.logic.has(Machine.recycling_machine) - else: - return True_() # You can always earn levels 1-5 with trash cans + if level <= 2: + return True_() + binning_rule = [self.logic.has(ModMachine.trash_bin) & self.logic.has(Machine.recycling_machine)] + if level > 4: + binning_rule.append(self.logic.has(ModMachine.composter)) + if level > 7: + binning_rule.append(self.logic.has(ModMachine.recycling_bin)) + if level > 9: + binning_rule.append(self.logic.has(ModMachine.advanced_recycling_machine)) + return And(*binning_rule) diff --git a/worlds/stardew_valley/mods/logic/special_orders_logic.py b/worlds/stardew_valley/mods/logic/special_orders_logic.py index e51a23d50254..1a0934282e09 100644 --- a/worlds/stardew_valley/mods/logic/special_orders_logic.py +++ b/worlds/stardew_valley/mods/logic/special_orders_logic.py @@ -1,12 +1,11 @@ from typing import Union -from ...data.craftable_data import all_crafting_recipes_by_name from ..mod_data import ModNames +from ...data.craftable_data import all_crafting_recipes_by_name from ...logic.action_logic import ActionLogicMixin from ...logic.artisan_logic import ArtisanLogicMixin from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.crafting_logic import CraftingLogicMixin -from ...logic.crop_logic import CropLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.received_logic import ReceivedLogicMixin from ...logic.region_logic import RegionLogicMixin @@ -34,7 +33,7 @@ def __init__(self, *args, **kwargs): self.special_order = ModSpecialOrderLogic(*args, **kwargs) -class ModSpecialOrderLogic(BaseLogic[Union[ActionLogicMixin, ArtisanLogicMixin, CraftingLogicMixin, CropLogicMixin, HasLogicMixin, RegionLogicMixin, +class ModSpecialOrderLogic(BaseLogic[Union[ActionLogicMixin, ArtisanLogicMixin, CraftingLogicMixin, HasLogicMixin, RegionLogicMixin, ReceivedLogicMixin, RelationshipLogicMixin, SeasonLogicMixin, WalletLogicMixin]]): def get_modded_special_orders_rules(self): special_orders = {} @@ -54,7 +53,7 @@ def get_modded_special_orders_rules(self): self.logic.region.can_reach(SVERegion.fairhaven_farm), ModSpecialOrder.a_mysterious_venture: self.logic.has(Bomb.cherry_bomb) & self.logic.has(Bomb.bomb) & self.logic.has(Bomb.mega_bomb) & self.logic.region.can_reach(Region.adventurer_guild), - ModSpecialOrder.an_elegant_reception: self.logic.artisan.can_keg(Fruit.starfruit) & self.logic.has(ArtisanGood.cheese) & + ModSpecialOrder.an_elegant_reception: self.logic.has(ArtisanGood.specific_wine(Fruit.starfruit)) & self.logic.has(ArtisanGood.cheese) & self.logic.has(ArtisanGood.goat_cheese) & self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.jenkins_cellar), ModSpecialOrder.fairy_garden: self.logic.has(Consumable.fairy_dust) & diff --git a/worlds/stardew_valley/mods/logic/sve_logic.py b/worlds/stardew_valley/mods/logic/sve_logic.py index 1254338fe2fc..fc093554d8e6 100644 --- a/worlds/stardew_valley/mods/logic/sve_logic.py +++ b/worlds/stardew_valley/mods/logic/sve_logic.py @@ -14,12 +14,11 @@ from ...logic.time_logic import TimeLogicMixin from ...logic.tool_logic import ToolLogicMixin from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem +from ...strings.quest_names import ModQuest from ...strings.quest_names import Quest from ...strings.region_names import Region from ...strings.tool_names import Tool, ToolMaterial from ...strings.wallet_item_names import Wallet -from ...stardew_rule import Or -from ...strings.quest_names import ModQuest class SVELogicMixin(BaseLogicMixin): @@ -29,7 +28,7 @@ def __init__(self, *args, **kwargs): class SVELogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, QuestLogicMixin, RegionLogicMixin, RelationshipLogicMixin, TimeLogicMixin, ToolLogicMixin, - CookingLogicMixin, MoneyLogicMixin, CombatLogicMixin, SeasonLogicMixin, QuestLogicMixin]]): + CookingLogicMixin, MoneyLogicMixin, CombatLogicMixin, SeasonLogicMixin]]): def initialize_rules(self): self.registry.sve_location_rules.update({ SVELocation.tempered_galaxy_sword: self.logic.money.can_spend_at(SVERegion.alesia_shop, 350000), @@ -39,17 +38,31 @@ def initialize_rules(self): def has_any_rune(self): rune_list = SVERunes.nexus_items - return Or(*(self.logic.received(rune) for rune in rune_list)) + return self.logic.or_(*(self.logic.received(rune) for rune in rune_list)) def has_iridium_bomb(self): if self.options.quest_locations < 0: return self.logic.quest.can_complete_quest(ModQuest.RailroadBoulder) return self.logic.received(SVEQuestItem.iridium_bomb) + def has_marlon_boat(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(ModQuest.MarlonsBoat) + return self.logic.received(SVEQuestItem.marlon_boat_paddle) + + def has_grandpa_shed_repaired(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(ModQuest.GrandpasShed) + return self.logic.received(SVEQuestItem.grandpa_shed) + + def has_bear_knowledge(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(Quest.strange_note) + return self.logic.received(Wallet.bears_knowledge) + def can_buy_bear_recipe(self): access_rule = (self.logic.quest.can_complete_quest(Quest.strange_note) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.basic) & self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.basic)) forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods, Region.mountain)) - knowledge_rule = self.logic.received(Wallet.bears_knowledge) + knowledge_rule = self.has_bear_knowledge() return access_rule & forage_rule & knowledge_rule - diff --git a/worlds/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py index a4d3b9828aa6..54408fb2c571 100644 --- a/worlds/stardew_valley/mods/mod_data.py +++ b/worlds/stardew_valley/mods/mod_data.py @@ -26,14 +26,3 @@ class ModNames: distant_lands = "Distant Lands - Witch Swamp Overhaul" lacey = "Hat Mouse Lacey" boarding_house = "Boarding House and Bus Stop Extension" - - jasper_sve = jasper + "," + sve - - -all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.alecto, - ModNames.distant_lands, ModNames.lacey, ModNames.boarding_house}) diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py index df0a12f6ef18..a402ba606868 100644 --- a/worlds/stardew_valley/mods/mod_regions.py +++ b/worlds/stardew_valley/mods/mod_regions.py @@ -1,11 +1,11 @@ from typing import Dict, List +from .mod_data import ModNames +from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \ JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \ AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion -from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData -from .mod_data import ModNames deep_woods_regions = [ RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]), @@ -179,15 +179,16 @@ RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]), RegionData(SVERegion.scarlett_house), RegionData(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]), - RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild]), - RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway]), - RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room]), - RegionData(SVERegion.first_slash_spare_room), - RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave]), - RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison]), - RegionData(SVERegion.dwarf_prison), - RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder]), - RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands]), + RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True), + RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True), + RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True), + RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True), + RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True), + RegionData(SVERegion.highlands_pond, is_ginger_island=True), + RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True), + RegionData(SVERegion.dwarf_prison, is_ginger_island=True), + RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True), + RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands], is_ginger_island=True), RegionData(SVERegion.forest_west, [SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora, SVEEntrance.use_bear_shop]), RegionData(SVERegion.aurora_vineyard, [SVEEntrance.to_aurora_basement]), @@ -217,7 +218,8 @@ ConnectionData(SVEEntrance.town_to_bridge, SVERegion.shearwater), ConnectionData(SVEEntrance.plot_to_bridge, SVERegion.shearwater), ConnectionData(SVEEntrance.bus_stop_to_shed, SVERegion.grandpas_shed), - ConnectionData(SVEEntrance.grandpa_shed_to_interior, SVERegion.grandpas_shed_interior, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(SVEEntrance.grandpa_shed_to_interior, SVERegion.grandpas_shed_interior, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town), ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), @@ -270,10 +272,12 @@ ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs), ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), + ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), + ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS), + ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond), ] alecto_regions = [ @@ -332,7 +336,8 @@ flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance, BoardingHouseRegion.abandoned_mines_entrance, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley, BoardingHouseRegion.lost_valley_minecart, flag=RandomizationFlag.BUILDINGS), + ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley, BoardingHouseRegion.lost_valley_minecart, + flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a, BoardingHouseRegion.abandoned_mines_1a, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b, BoardingHouseRegion.abandoned_mines_1b, flag=RandomizationFlag.BUILDINGS), @@ -347,16 +352,15 @@ ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS) - - ] vanilla_connections_to_remove_by_mod: Dict[str, List[ConnectionData]] = { - ModNames.sve: [ConnectionData(Entrance.mountain_to_the_mines, Region.mines, - flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, - flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ] + ModNames.sve: [ + ConnectionData(Entrance.mountain_to_the_mines, Region.mines, + flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ] } ModDataList = { diff --git a/worlds/stardew_valley/option_groups.py b/worlds/stardew_valley/option_groups.py deleted file mode 100644 index 50709c10fd49..000000000000 --- a/worlds/stardew_valley/option_groups.py +++ /dev/null @@ -1,65 +0,0 @@ -from Options import OptionGroup, DeathLink, ProgressionBalancing, Accessibility -from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, - EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression, - ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, - FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations, - QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, - NumberOfMovementBuffs, NumberOfLuckBuffs, ExcludeGingerIsland, TrapItems, - MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, - FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType, - Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods) - -sv_option_groups = [ - OptionGroup("General", [ - Goal, - FarmType, - BundleRandomization, - BundlePrice, - EntranceRandomization, - ExcludeGingerIsland, - ]), - OptionGroup("Major Unlocks", [ - SeasonRandomization, - Cropsanity, - BackpackProgression, - ToolProgression, - ElevatorProgression, - SkillProgression, - BuildingProgression, - ]), - OptionGroup("Extra Shuffling", [ - FestivalLocations, - ArcadeMachineLocations, - SpecialOrderLocations, - QuestLocations, - Fishsanity, - Museumsanity, - Friendsanity, - FriendsanityHeartSize, - Monstersanity, - Shipsanity, - Cooksanity, - Chefsanity, - Craftsanity, - ]), - OptionGroup("Multipliers and Buffs", [ - StartingMoney, - ProfitMargin, - ExperienceMultiplier, - FriendshipMultiplier, - DebrisMultiplier, - NumberOfMovementBuffs, - NumberOfLuckBuffs, - TrapItems, - MultipleDaySleepEnabled, - MultipleDaySleepCost, - QuickStart, - ]), - OptionGroup("Advanced Options", [ - Gifting, - DeathLink, - Mods, - ProgressionBalancing, - Accessibility, - ]), -] diff --git a/worlds/stardew_valley/options/__init__.py b/worlds/stardew_valley/options/__init__.py new file mode 100644 index 000000000000..d1436b00dff7 --- /dev/null +++ b/worlds/stardew_valley/options/__init__.py @@ -0,0 +1,6 @@ +from .options import StardewValleyOption, Goal, FarmType, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, \ + SeasonRandomization, Cropsanity, BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, \ + ArcadeMachineLocations, SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, \ + Friendsanity, FriendsanityHeartSize, Booksanity, Walnutsanity, NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems, \ + MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, Mods, BundlePlando, \ + StardewValleyOptions diff --git a/worlds/stardew_valley/options/forced_options.py b/worlds/stardew_valley/options/forced_options.py new file mode 100644 index 000000000000..7429f3cbfc65 --- /dev/null +++ b/worlds/stardew_valley/options/forced_options.py @@ -0,0 +1,60 @@ +import logging + +import Options as ap_options +from . import options + +logger = logging.getLogger(__name__) + + +def force_change_options_if_incompatible(world_options: options.StardewValleyOptions, player: int, player_name: str) -> None: + force_ginger_island_inclusion_when_goal_is_ginger_island_related(world_options, player, player_name) + force_walnutsanity_deactivation_when_ginger_island_is_excluded(world_options, player, player_name) + force_qi_special_orders_deactivation_when_ginger_island_is_excluded(world_options, player, player_name) + force_accessibility_to_full_when_goal_requires_all_locations(player, player_name, world_options) + + +def force_ginger_island_inclusion_when_goal_is_ginger_island_related(world_options: options.StardewValleyOptions, player: int, player_name: str) -> None: + goal_is_walnut_hunter = world_options.goal == options.Goal.option_greatest_walnut_hunter + goal_is_perfection = world_options.goal == options.Goal.option_perfection + goal_is_island_related = goal_is_walnut_hunter or goal_is_perfection + ginger_island_is_excluded = world_options.exclude_ginger_island == options.ExcludeGingerIsland.option_true + + if goal_is_island_related and ginger_island_is_excluded: + world_options.exclude_ginger_island.value = options.ExcludeGingerIsland.option_false + goal_name = world_options.goal.current_option_name + logger.warning(f"Goal '{goal_name}' requires Ginger Island. " + f"Exclude Ginger Island option forced to 'False' for player {player} ({player_name})") + + +def force_walnutsanity_deactivation_when_ginger_island_is_excluded(world_options: options.StardewValleyOptions, player: int, player_name: str): + ginger_island_is_excluded = world_options.exclude_ginger_island == options.ExcludeGingerIsland.option_true + walnutsanity_is_active = world_options.walnutsanity != options.Walnutsanity.preset_none + + if ginger_island_is_excluded and walnutsanity_is_active: + world_options.walnutsanity.value = options.Walnutsanity.preset_none + logger.warning(f"Walnutsanity requires Ginger Island. " + f"Ginger Island was excluded from {player} ({player_name})'s world, so walnutsanity was force disabled") + + +def force_qi_special_orders_deactivation_when_ginger_island_is_excluded(world_options: options.StardewValleyOptions, player: int, player_name: str): + ginger_island_is_excluded = world_options.exclude_ginger_island == options.ExcludeGingerIsland.option_true + qi_board_is_active = world_options.special_order_locations.value & options.SpecialOrderLocations.value_qi + + if ginger_island_is_excluded and qi_board_is_active: + original_option_name = world_options.special_order_locations.current_option_name + world_options.special_order_locations.value -= options.SpecialOrderLocations.value_qi + logger.warning(f"Mr. Qi's Special Orders requires Ginger Island. " + f"Ginger Island was excluded from {player} ({player_name})'s world, so Special Order Locations was changed from {original_option_name} to {world_options.special_order_locations.current_option_name}") + + +def force_accessibility_to_full_when_goal_requires_all_locations(player, player_name, world_options): + goal_is_allsanity = world_options.goal == options.Goal.option_allsanity + goal_is_perfection = world_options.goal == options.Goal.option_perfection + goal_requires_all_locations = goal_is_allsanity or goal_is_perfection + accessibility_is_minimal = world_options.accessibility == ap_options.Accessibility.option_minimal + + if goal_requires_all_locations and accessibility_is_minimal: + world_options.accessibility.value = ap_options.Accessibility.option_full + goal_name = world_options.goal.current_option_name + logger.warning(f"Goal '{goal_name}' requires full accessibility. " + f"Accessibility option forced to 'Full' for player {player} ({player_name})") diff --git a/worlds/stardew_valley/options/option_groups.py b/worlds/stardew_valley/options/option_groups.py new file mode 100644 index 000000000000..bcb9bee77ff4 --- /dev/null +++ b/worlds/stardew_valley/options/option_groups.py @@ -0,0 +1,68 @@ +import logging + +import Options as ap_options +from . import options + +sv_option_groups = [] +try: + from Options import OptionGroup +except ImportError: + logging.warning("Old AP Version, OptionGroup not available.") +else: + sv_option_groups = [ + OptionGroup("General", [ + options.Goal, + options.FarmType, + options.BundleRandomization, + options.BundlePrice, + options.EntranceRandomization, + options.ExcludeGingerIsland, + ]), + OptionGroup("Major Unlocks", [ + options.SeasonRandomization, + options.Cropsanity, + options.BackpackProgression, + options.ToolProgression, + options.ElevatorProgression, + options.SkillProgression, + options.BuildingProgression, + ]), + OptionGroup("Extra Shuffling", [ + options.FestivalLocations, + options.ArcadeMachineLocations, + options.SpecialOrderLocations, + options.QuestLocations, + options.Fishsanity, + options.Museumsanity, + options.Friendsanity, + options.FriendsanityHeartSize, + options.Monstersanity, + options.Shipsanity, + options.Cooksanity, + options.Chefsanity, + options.Craftsanity, + options.Booksanity, + options.Walnutsanity, + ]), + OptionGroup("Multipliers and Buffs", [ + options.StartingMoney, + options.ProfitMargin, + options.ExperienceMultiplier, + options.FriendshipMultiplier, + options.DebrisMultiplier, + options.NumberOfMovementBuffs, + options.EnabledFillerBuffs, + options.TrapItems, + options.MultipleDaySleepEnabled, + options.MultipleDaySleepCost, + options.QuickStart, + ]), + OptionGroup("Advanced Options", [ + options.Gifting, + ap_options.DeathLink, + options.Mods, + options.BundlePlando, + ap_options.ProgressionBalancing, + ap_options.Accessibility, + ]), + ] diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options/options.py similarity index 79% rename from worlds/stardew_valley/options.py rename to worlds/stardew_valley/options/options.py index ba1ebfb9c177..5d3b25b4da13 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options/options.py @@ -1,8 +1,12 @@ +import sys +import typing from dataclasses import dataclass from typing import Protocol, ClassVar -from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink -from .mods.mod_data import ModNames +from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, OptionList, Visibility +from ..mods.mod_data import ModNames +from ..strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName +from ..strings.bundle_names import all_cc_bundle_names class StardewValleyOption(Protocol): @@ -18,7 +22,7 @@ class Goal(Choice): Master Angler: Catch every fish. Adapts to Fishsanity Complete Collection: Complete the museum collection Full House: Get married and have 2 children - Greatest Walnut Hunter: Find 130 Golden Walnuts + Greatest Walnut Hunter: Find 130 Golden Walnuts. Pairs well with Walnutsanity Protector of the Valley: Complete the monster slayer goals. Adapts to Monstersanity Full Shipment: Ship every item. Adapts to Shipsanity Gourmet Chef: Cook every recipe. Adapts to Cooksanity @@ -73,6 +77,7 @@ class FarmType(Choice): option_wilderness = 4 option_four_corners = 5 option_beach = 6 + option_meadowlands = 7 class StartingMoney(NamedRange): @@ -118,14 +123,16 @@ class BundleRandomization(Choice): Vanilla: Standard bundles from the vanilla game Thematic: Every bundle will require random items compatible with their original theme Remixed: Picks bundles at random from thematic, vanilla remixed and new custom ones + Remixed Anywhere: Remixed, but bundles are not locked to specific rooms. Shuffled: Every bundle will require random items and follow no particular structure""" internal_name = "bundle_randomization" display_name = "Bundle Randomization" - default = 2 option_vanilla = 0 option_thematic = 1 - option_remixed = 2 - option_shuffled = 3 + option_remixed = 3 + option_remixed_anywhere = 4 + option_shuffled = 6 + default = option_remixed class BundlePrice(Choice): @@ -155,6 +162,7 @@ class EntranceRandomization(Choice): Pelican Town: Only doors in the main town area are randomized with each other Non Progression: Only entrances that are always available are randomized with each other Buildings: All entrances that allow you to enter a building are randomized with each other + Buildings Without House: Buildings, but excluding the farmhouse Chaos: Same as "Buildings", but the entrances get reshuffled every single day! """ # Everything: All buildings and areas are randomized with each other @@ -169,9 +177,10 @@ class EntranceRandomization(Choice): option_disabled = 0 option_pelican_town = 1 option_non_progression = 2 - option_buildings = 3 - # option_everything = 4 - option_chaos = 5 + option_buildings_without_house = 3 + option_buildings = 4 + # option_everything = 10 + option_chaos = 12 # option_buildings_one_way = 6 # option_everything_one_way = 7 # option_chaos_one_way = 8 @@ -255,12 +264,14 @@ class ElevatorProgression(Choice): class SkillProgression(Choice): """Shuffle skill levels? Vanilla: Leveling up skills is normal - Progressive: Skill levels are unlocked randomly, and earning xp sends checks""" + Progressive: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are excluded + With Masteries: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are included""" internal_name = "skill_progression" display_name = "Skill Progression" - default = 1 + default = 2 option_vanilla = 0 option_progressive = 1 + option_progressive_with_masteries = 2 class BuildingProgression(Choice): @@ -319,13 +330,26 @@ class SpecialOrderLocations(Choice): Disabled: The special orders are not included in the Archipelago shuffling. Board Only: The Special Orders on the board in town are location checks Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town + Short: All Special Order requirements are reduced by 40% + Very Short: All Special Order requirements are reduced by 80% """ internal_name = "special_order_locations" display_name = "Special Order Locations" - default = 1 - option_disabled = 0 - option_board_only = 1 - option_board_qi = 2 + option_vanilla = 0b0000 # 0 + option_board = 0b0001 # 1 + value_qi = 0b0010 # 2 + value_short = 0b0100 # 4 + value_very_short = 0b1000 # 8 + option_board_qi = option_board | value_qi # 3 + option_vanilla_short = value_short # 4 + option_board_short = option_board | value_short # 5 + option_board_qi_short = option_board_qi | value_short # 7 + option_vanilla_very_short = value_very_short # 8 + option_board_very_short = option_board | value_very_short # 9 + option_board_qi_very_short = option_board_qi | value_very_short # 11 + alias_disabled = option_vanilla + alias_board_only = option_board + default = option_board_short class QuestLocations(NamedRange): @@ -533,6 +557,48 @@ class FriendsanityHeartSize(Range): # step = 1 +class Booksanity(Choice): + """Shuffle Books? + None: All books behave like vanilla + Power: Power books are turned into checks + Power and Skill: Power and skill books are turned into checks. + All: Lost books are also included in the shuffling + """ + internal_name = "booksanity" + display_name = "Booksanity" + default = 2 + option_none = 0 + option_power = 1 + option_power_skill = 2 + option_all = 3 + + +class Walnutsanity(OptionSet): + """Shuffle walnuts? + Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame + Bushes: Walnuts that are in a bush and can be collected by clicking it + Dig spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts + Repeatables: Random chance walnuts from normal actions (fishing, farming, combat, etc) + """ + internal_name = "walnutsanity" + display_name = "Walnutsanity" + valid_keys = frozenset({ + WalnutsanityOptionName.puzzles, WalnutsanityOptionName.bushes, WalnutsanityOptionName.dig_spots, + WalnutsanityOptionName.repeatables, + }) + preset_none = frozenset() + preset_all = valid_keys + default = preset_none + + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, OptionSet): + return set(self.value) == other.value + if isinstance(other, OptionList): + return set(self.value) == set(other.value) + else: + return typing.cast(bool, self.value == other) + + class NumberOfMovementBuffs(Range): """Number of movement speed buffs to the player that exist as items in the pool. Each movement speed buff is a +25% multiplier that stacks additively""" @@ -544,15 +610,28 @@ class NumberOfMovementBuffs(Range): # step = 1 -class NumberOfLuckBuffs(Range): - """Number of luck buffs to the player that exist as items in the pool. - Each luck buff is a bonus to daily luck of 0.025""" - internal_name = "luck_buff_number" - display_name = "Number of Luck Buffs" - range_start = 0 - range_end = 12 - default = 4 - # step = 1 +class EnabledFillerBuffs(OptionSet): + """Enable various permanent player buffs to roll as filler items + Luck: Increase daily luck + Damage: Increased Damage % + Defense: Increased Defense + Immunity: Increased Immunity + Health: Increased Max Health + Energy: Increased Max Energy + Bite Rate: Shorter delay to get a bite when fishing + Fish Trap: Effect similar to the Trap Bobber, but weaker + Fishing Bar Size: Increased Fishing Bar Size + """ + internal_name = "enabled_filler_buffs" + display_name = "Enabled Filler Buffs" + valid_keys = frozenset({ + BuffOptionName.luck, BuffOptionName.damage, BuffOptionName.defense, BuffOptionName.immunity, BuffOptionName.health, + BuffOptionName.energy, BuffOptionName.bite, BuffOptionName.fish_trap, BuffOptionName.fishing_bar, + }) + # OptionName.buff_quality, OptionName.buff_glow}) # Disabled these two buffs because they are too hard to make on the mod side + preset_none = frozenset() + preset_all = valid_keys + default = frozenset({BuffOptionName.luck, BuffOptionName.defense, BuffOptionName.bite}) class ExcludeGingerIsland(Toggle): @@ -678,19 +757,39 @@ class Gifting(Toggle): default = 1 +# These mods have been disabled because either they are not updated for the current supported version of Stardew Valley, +# or we didn't find the time to validate that they work or fix compatibility issues if they do. +# Once a mod is validated to be functional, it can simply be removed from this list +disabled_mods = {ModNames.deepwoods, ModNames.magic, + ModNames.cooking_skill, + ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.shiko, ModNames.delores, ModNames.riley, + ModNames.boarding_house} + +if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys(): + disabled_mods = {} + + class Mods(OptionSet): """List of mods that will be included in the shuffling.""" internal_name = "mods" display_name = "Mods" - valid_keys = { - ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.distant_lands, - ModNames.alecto, ModNames.lacey, ModNames.boarding_house - } + valid_keys = {ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator, ModNames.sve, ModNames.distant_lands, + ModNames.alecto, ModNames.lacey, ModNames.boarding_house}.difference(disabled_mods) + + +class BundlePlando(OptionSet): + """If using Remixed bundles, this guarantees some of them will show up in your community center. + If more bundles are specified than what fits in their parent room, that room will randomly pick from only the plando ones""" + internal_name = "bundle_plando" + display_name = "Bundle Plando" + visibility = Visibility.template | Visibility.spoiler + valid_keys = set(all_cc_bundle_names) @dataclass @@ -720,6 +819,8 @@ class StardewValleyOptions(PerGameCommonOptions): craftsanity: Craftsanity friendsanity: Friendsanity friendsanity_heart_size: FriendsanityHeartSize + booksanity: Booksanity + walnutsanity: Walnutsanity exclude_ginger_island: ExcludeGingerIsland quick_start: QuickStart starting_money: StartingMoney @@ -728,10 +829,11 @@ class StardewValleyOptions(PerGameCommonOptions): friendship_multiplier: FriendshipMultiplier debris_multiplier: DebrisMultiplier movement_buff_number: NumberOfMovementBuffs - luck_buff_number: NumberOfLuckBuffs + enabled_filler_buffs: EnabledFillerBuffs trap_items: TrapItems multiple_day_sleep_enabled: MultipleDaySleepEnabled multiple_day_sleep_cost: MultipleDaySleepCost gifting: Gifting mods: Mods + bundle_plando: BundlePlando death_link: DeathLink diff --git a/worlds/stardew_valley/options/presets.py b/worlds/stardew_valley/options/presets.py new file mode 100644 index 000000000000..c2c210e5ca6e --- /dev/null +++ b/worlds/stardew_valley/options/presets.py @@ -0,0 +1,371 @@ +from typing import Any, Dict + +import Options as ap_options +from . import options +from ..strings.ap_names.ap_option_names import WalnutsanityOptionName + +# @formatter:off +all_random_settings = { + "progression_balancing": "random", + "accessibility": "random", + options.Goal.internal_name: "random", + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "random", + options.ProfitMargin.internal_name: "random", + options.BundleRandomization.internal_name: "random", + options.BundlePrice.internal_name: "random", + options.EntranceRandomization.internal_name: "random", + options.SeasonRandomization.internal_name: "random", + options.Cropsanity.internal_name: "random", + options.BackpackProgression.internal_name: "random", + options.ToolProgression.internal_name: "random", + options.ElevatorProgression.internal_name: "random", + options.SkillProgression.internal_name: "random", + options.BuildingProgression.internal_name: "random", + options.FestivalLocations.internal_name: "random", + options.ArcadeMachineLocations.internal_name: "random", + options.SpecialOrderLocations.internal_name: "random", + options.QuestLocations.internal_name: "random", + options.Fishsanity.internal_name: "random", + options.Museumsanity.internal_name: "random", + options.Monstersanity.internal_name: "random", + options.Shipsanity.internal_name: "random", + options.Cooksanity.internal_name: "random", + options.Chefsanity.internal_name: "random", + options.Craftsanity.internal_name: "random", + options.Friendsanity.internal_name: "random", + options.FriendsanityHeartSize.internal_name: "random", + options.Booksanity.internal_name: "random", + options.NumberOfMovementBuffs.internal_name: "random", + options.ExcludeGingerIsland.internal_name: "random", + options.TrapItems.internal_name: "random", + options.MultipleDaySleepEnabled.internal_name: "random", + options.MultipleDaySleepCost.internal_name: "random", + options.ExperienceMultiplier.internal_name: "random", + options.FriendshipMultiplier.internal_name: "random", + options.DebrisMultiplier.internal_name: "random", + options.QuickStart.internal_name: "random", + options.Gifting.internal_name: "random", + "death_link": "random", +} + +easy_settings = { + options.Goal.internal_name: options.Goal.option_community_center, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "very rich", + options.ProfitMargin.internal_name: "double", + options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, + options.BundlePrice.internal_name: options.BundlePrice.option_cheap, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive_very_cheap, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla_very_short, + options.QuestLocations.internal_name: "minimum", + options.Fishsanity.internal_name: options.Fishsanity.option_only_easy_fish, + options.Museumsanity.internal_name: options.Museumsanity.option_milestones, + options.Monstersanity.internal_name: options.Monstersanity.option_one_per_category, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_none, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none, + options.NumberOfMovementBuffs.internal_name: 8, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_easy, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "free", + options.ExperienceMultiplier.internal_name: "triple", + options.FriendshipMultiplier.internal_name: "quadruple", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_quarter, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "false", +} + +medium_settings = { + options.Goal.internal_name: options.Goal.option_community_center, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "rich", + options.ProfitMargin.internal_name: 150, + options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, + options.BundlePrice.internal_name: options.BundlePrice.option_normal, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_non_progression, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive_cheap, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_cheap, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_victories_easy, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_short, + options.QuestLocations.internal_name: "normal", + options.Fishsanity.internal_name: options.Fishsanity.option_exclude_legendaries, + options.Museumsanity.internal_name: options.Museumsanity.option_milestones, + options.Monstersanity.internal_name: options.Monstersanity.option_one_per_monster, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_queen_of_sauce, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_power_skill, + options.Walnutsanity.internal_name: [WalnutsanityOptionName.puzzles], + options.NumberOfMovementBuffs.internal_name: 6, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_medium, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "free", + options.ExperienceMultiplier.internal_name: "double", + options.FriendshipMultiplier.internal_name: "triple", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_half, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "false", +} + +hard_settings = { + options.Goal.internal_name: options.Goal.option_grandpa_evaluation, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "extra", + options.ProfitMargin.internal_name: "normal", + options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings_without_house, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive_from_previous_floor, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi_short, + options.QuestLocations.internal_name: "lots", + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, + options.Shipsanity.internal_name: options.Shipsanity.option_crops, + options.Cooksanity.internal_name: options.Cooksanity.option_queen_of_sauce, + options.Chefsanity.internal_name: options.Chefsanity.option_qos_and_purchases, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_all, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all, + options.NumberOfMovementBuffs.internal_name: 4, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_hard, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "cheap", + options.ExperienceMultiplier.internal_name: "vanilla", + options.FriendshipMultiplier.internal_name: "double", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_vanilla, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "true", +} + +nightmare_settings = { + options.Goal.internal_name: options.Goal.option_community_center, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "vanilla", + options.ProfitMargin.internal_name: "half", + options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, + options.BundlePrice.internal_name: options.BundlePrice.option_very_expensive, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive_from_previous_floor, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.QuestLocations.internal_name: "maximum", + options.Fishsanity.internal_name: options.Fishsanity.option_special, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.Monstersanity.internal_name: options.Monstersanity.option_split_goals, + options.Shipsanity.internal_name: options.Shipsanity.option_full_shipment_with_fish, + options.Cooksanity.internal_name: options.Cooksanity.option_queen_of_sauce, + options.Chefsanity.internal_name: options.Chefsanity.option_qos_and_purchases, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all, + options.NumberOfMovementBuffs.internal_name: 2, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.option_hell, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "expensive", + options.ExperienceMultiplier.internal_name: "half", + options.FriendshipMultiplier.internal_name: "vanilla", + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_vanilla, + options.QuickStart.internal_name: options.QuickStart.option_false, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "true", +} + +short_settings = { + options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: "filthy rich", + options.ProfitMargin.internal_name: "quadruple", + options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, + options.BundlePrice.internal_name: options.BundlePrice.option_minimum, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized_not_winter, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive_very_cheap, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla_very_short, + options.QuestLocations.internal_name: "none", + options.Fishsanity.internal_name: options.Fishsanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_none, + options.FriendsanityHeartSize.internal_name: 4, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none, + options.NumberOfMovementBuffs.internal_name: 10, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.option_easy, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.option_true, + options.MultipleDaySleepCost.internal_name: "free", + options.ExperienceMultiplier.internal_name: "quadruple", + options.FriendshipMultiplier.internal_name: 800, + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.option_none, + options.QuickStart.internal_name: options.QuickStart.option_true, + options.Gifting.internal_name: options.Gifting.option_true, + "death_link": "false", +} + +minsanity_settings = { + options.Goal.internal_name: options.Goal.default, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: options.StartingMoney.default, + options.ProfitMargin.internal_name: options.ProfitMargin.default, + options.BundleRandomization.internal_name: options.BundleRandomization.default, + options.BundlePrice.internal_name: options.BundlePrice.default, + options.EntranceRandomization.internal_name: options.EntranceRandomization.default, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla_very_short, + options.QuestLocations.internal_name: "none", + options.Fishsanity.internal_name: options.Fishsanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Friendsanity.internal_name: options.Friendsanity.option_none, + options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none, + options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.TrapItems.internal_name: options.TrapItems.default, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.default, + options.MultipleDaySleepCost.internal_name: options.MultipleDaySleepCost.default, + options.ExperienceMultiplier.internal_name: options.ExperienceMultiplier.default, + options.FriendshipMultiplier.internal_name: options.FriendshipMultiplier.default, + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.default, + options.QuickStart.internal_name: options.QuickStart.default, + options.Gifting.internal_name: options.Gifting.default, + "death_link": ap_options.DeathLink.default, +} + +allsanity_settings = { + options.Goal.internal_name: options.Goal.default, + options.FarmType.internal_name: "random", + options.StartingMoney.internal_name: options.StartingMoney.default, + options.ProfitMargin.internal_name: options.ProfitMargin.default, + options.BundleRandomization.internal_name: options.BundleRandomization.default, + options.BundlePrice.internal_name: options.BundlePrice.default, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.QuestLocations.internal_name: "maximum", + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, + options.Shipsanity.internal_name: options.Shipsanity.option_everything, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + options.Chefsanity.internal_name: options.Chefsanity.option_all, + options.Craftsanity.internal_name: options.Craftsanity.option_all, + options.Friendsanity.internal_name: options.Friendsanity.option_all, + options.FriendsanityHeartSize.internal_name: 1, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all, + options.NumberOfMovementBuffs.internal_name: 12, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.TrapItems.internal_name: options.TrapItems.default, + options.MultipleDaySleepEnabled.internal_name: options.MultipleDaySleepEnabled.default, + options.MultipleDaySleepCost.internal_name: options.MultipleDaySleepCost.default, + options.ExperienceMultiplier.internal_name: options.ExperienceMultiplier.default, + options.FriendshipMultiplier.internal_name: options.FriendshipMultiplier.default, + options.DebrisMultiplier.internal_name: options.DebrisMultiplier.default, + options.QuickStart.internal_name: options.QuickStart.default, + options.Gifting.internal_name: options.Gifting.default, + "death_link": ap_options.DeathLink.default, +} +# @formatter:on + + +sv_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Easy": easy_settings, + "Medium": medium_settings, + "Hard": hard_settings, + "Nightmare": nightmare_settings, + "Short": short_settings, + "Minsanity": minsanity_settings, + "Allsanity": allsanity_settings, +} diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py deleted file mode 100644 index e75eb5c5fcde..000000000000 --- a/worlds/stardew_valley/presets.py +++ /dev/null @@ -1,371 +0,0 @@ -from typing import Any, Dict - -from Options import Accessibility, ProgressionBalancing, DeathLink -from .options import Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, \ - BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, \ - SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, NumberOfLuckBuffs, \ - ExcludeGingerIsland, TrapItems, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ - Gifting, FarmType, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity - -all_random_settings = { - "progression_balancing": "random", - "accessibility": "random", - Goal.internal_name: "random", - FarmType.internal_name: "random", - StartingMoney.internal_name: "random", - ProfitMargin.internal_name: "random", - BundleRandomization.internal_name: "random", - BundlePrice.internal_name: "random", - EntranceRandomization.internal_name: "random", - SeasonRandomization.internal_name: "random", - Cropsanity.internal_name: "random", - BackpackProgression.internal_name: "random", - ToolProgression.internal_name: "random", - ElevatorProgression.internal_name: "random", - SkillProgression.internal_name: "random", - BuildingProgression.internal_name: "random", - FestivalLocations.internal_name: "random", - ArcadeMachineLocations.internal_name: "random", - SpecialOrderLocations.internal_name: "random", - QuestLocations.internal_name: "random", - Fishsanity.internal_name: "random", - Museumsanity.internal_name: "random", - Monstersanity.internal_name: "random", - Shipsanity.internal_name: "random", - Cooksanity.internal_name: "random", - Chefsanity.internal_name: "random", - Craftsanity.internal_name: "random", - Friendsanity.internal_name: "random", - FriendsanityHeartSize.internal_name: "random", - NumberOfMovementBuffs.internal_name: "random", - NumberOfLuckBuffs.internal_name: "random", - ExcludeGingerIsland.internal_name: "random", - TrapItems.internal_name: "random", - MultipleDaySleepEnabled.internal_name: "random", - MultipleDaySleepCost.internal_name: "random", - ExperienceMultiplier.internal_name: "random", - FriendshipMultiplier.internal_name: "random", - DebrisMultiplier.internal_name: "random", - QuickStart.internal_name: "random", - Gifting.internal_name: "random", - "death_link": "random", -} - -easy_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_items, - Goal.internal_name: Goal.option_community_center, - FarmType.internal_name: "random", - StartingMoney.internal_name: "very rich", - ProfitMargin.internal_name: "double", - BundleRandomization.internal_name: BundleRandomization.option_thematic, - BundlePrice.internal_name: BundlePrice.option_cheap, - EntranceRandomization.internal_name: EntranceRandomization.option_disabled, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, - FestivalLocations.internal_name: FestivalLocations.option_easy, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, - QuestLocations.internal_name: "minimum", - Fishsanity.internal_name: Fishsanity.option_only_easy_fish, - Museumsanity.internal_name: Museumsanity.option_milestones, - Monstersanity.internal_name: Monstersanity.option_one_per_category, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_none, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - FriendsanityHeartSize.internal_name: 4, - NumberOfMovementBuffs.internal_name: 8, - NumberOfLuckBuffs.internal_name: 8, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.option_easy, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "free", - ExperienceMultiplier.internal_name: "triple", - FriendshipMultiplier.internal_name: "quadruple", - DebrisMultiplier.internal_name: DebrisMultiplier.option_quarter, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "false", -} - -medium_settings = { - "progression_balancing": 25, - "accessibility": Accessibility.option_locations, - Goal.internal_name: Goal.option_community_center, - FarmType.internal_name: "random", - StartingMoney.internal_name: "rich", - ProfitMargin.internal_name: 150, - BundleRandomization.internal_name: BundleRandomization.option_remixed, - BundlePrice.internal_name: BundlePrice.option_normal, - EntranceRandomization.internal_name: EntranceRandomization.option_non_progression, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories_easy, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only, - QuestLocations.internal_name: "normal", - Fishsanity.internal_name: Fishsanity.option_exclude_legendaries, - Museumsanity.internal_name: Museumsanity.option_milestones, - Monstersanity.internal_name: Monstersanity.option_one_per_monster, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_queen_of_sauce, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_starting_npcs, - FriendsanityHeartSize.internal_name: 4, - NumberOfMovementBuffs.internal_name: 6, - NumberOfLuckBuffs.internal_name: 6, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.option_medium, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "free", - ExperienceMultiplier.internal_name: "double", - FriendshipMultiplier.internal_name: "triple", - DebrisMultiplier.internal_name: DebrisMultiplier.option_half, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "false", -} - -hard_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_locations, - Goal.internal_name: Goal.option_grandpa_evaluation, - FarmType.internal_name: "random", - StartingMoney.internal_name: "extra", - ProfitMargin.internal_name: "normal", - BundleRandomization.internal_name: BundleRandomization.option_remixed, - BundlePrice.internal_name: BundlePrice.option_expensive, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - QuestLocations.internal_name: "lots", - Fishsanity.internal_name: Fishsanity.option_all, - Museumsanity.internal_name: Museumsanity.option_all, - Monstersanity.internal_name: Monstersanity.option_progressive_goals, - Shipsanity.internal_name: Shipsanity.option_crops, - Cooksanity.internal_name: Cooksanity.option_queen_of_sauce, - Chefsanity.internal_name: Chefsanity.option_qos_and_purchases, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - NumberOfMovementBuffs.internal_name: 4, - NumberOfLuckBuffs.internal_name: 4, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.option_hard, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "cheap", - ExperienceMultiplier.internal_name: "vanilla", - FriendshipMultiplier.internal_name: "double", - DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "true", -} - -nightmare_settings = { - "progression_balancing": 0, - "accessibility": Accessibility.option_locations, - Goal.internal_name: Goal.option_community_center, - FarmType.internal_name: "random", - StartingMoney.internal_name: "vanilla", - ProfitMargin.internal_name: "half", - BundleRandomization.internal_name: BundleRandomization.option_shuffled, - BundlePrice.internal_name: BundlePrice.option_very_expensive, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - QuestLocations.internal_name: "maximum", - Fishsanity.internal_name: Fishsanity.option_special, - Museumsanity.internal_name: Museumsanity.option_all, - Monstersanity.internal_name: Monstersanity.option_split_goals, - Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, - Cooksanity.internal_name: Cooksanity.option_queen_of_sauce, - Chefsanity.internal_name: Chefsanity.option_qos_and_purchases, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 4, - NumberOfMovementBuffs.internal_name: 2, - NumberOfLuckBuffs.internal_name: 2, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.option_hell, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "expensive", - ExperienceMultiplier.internal_name: "half", - FriendshipMultiplier.internal_name: "vanilla", - DebrisMultiplier.internal_name: DebrisMultiplier.option_vanilla, - QuickStart.internal_name: QuickStart.option_false, - Gifting.internal_name: Gifting.option_true, - "death_link": "true", -} - -short_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_items, - Goal.internal_name: Goal.option_bottom_of_the_mines, - FarmType.internal_name: "random", - StartingMoney.internal_name: "filthy rich", - ProfitMargin.internal_name: "quadruple", - BundleRandomization.internal_name: BundleRandomization.option_remixed, - BundlePrice.internal_name: BundlePrice.option_minimum, - EntranceRandomization.internal_name: EntranceRandomization.option_disabled, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, - Cropsanity.internal_name: Cropsanity.option_disabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, - FestivalLocations.internal_name: FestivalLocations.option_disabled, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, - QuestLocations.internal_name: "none", - Fishsanity.internal_name: Fishsanity.option_none, - Museumsanity.internal_name: Museumsanity.option_none, - Monstersanity.internal_name: Monstersanity.option_none, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_none, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - FriendsanityHeartSize.internal_name: 4, - NumberOfMovementBuffs.internal_name: 10, - NumberOfLuckBuffs.internal_name: 10, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.option_easy, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, - MultipleDaySleepCost.internal_name: "free", - ExperienceMultiplier.internal_name: "quadruple", - FriendshipMultiplier.internal_name: 800, - DebrisMultiplier.internal_name: DebrisMultiplier.option_none, - QuickStart.internal_name: QuickStart.option_true, - Gifting.internal_name: Gifting.option_true, - "death_link": "false", -} - -minsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_minimal, - Goal.internal_name: Goal.default, - FarmType.internal_name: "random", - StartingMoney.internal_name: StartingMoney.default, - ProfitMargin.internal_name: ProfitMargin.default, - BundleRandomization.internal_name: BundleRandomization.default, - BundlePrice.internal_name: BundlePrice.default, - EntranceRandomization.internal_name: EntranceRandomization.default, - SeasonRandomization.internal_name: SeasonRandomization.option_disabled, - Cropsanity.internal_name: Cropsanity.option_disabled, - BackpackProgression.internal_name: BackpackProgression.option_vanilla, - ToolProgression.internal_name: ToolProgression.option_vanilla, - ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, - SkillProgression.internal_name: SkillProgression.option_vanilla, - BuildingProgression.internal_name: BuildingProgression.option_vanilla, - FestivalLocations.internal_name: FestivalLocations.option_disabled, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, - QuestLocations.internal_name: "none", - Fishsanity.internal_name: Fishsanity.option_none, - Museumsanity.internal_name: Museumsanity.option_none, - Monstersanity.internal_name: Monstersanity.option_none, - Shipsanity.internal_name: Shipsanity.option_none, - Cooksanity.internal_name: Cooksanity.option_none, - Chefsanity.internal_name: Chefsanity.option_none, - Craftsanity.internal_name: Craftsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - FriendsanityHeartSize.internal_name: FriendsanityHeartSize.default, - NumberOfMovementBuffs.internal_name: NumberOfMovementBuffs.default, - NumberOfLuckBuffs.internal_name: NumberOfLuckBuffs.default, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - TrapItems.internal_name: TrapItems.default, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, - MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, - ExperienceMultiplier.internal_name: ExperienceMultiplier.default, - FriendshipMultiplier.internal_name: FriendshipMultiplier.default, - DebrisMultiplier.internal_name: DebrisMultiplier.default, - QuickStart.internal_name: QuickStart.default, - Gifting.internal_name: Gifting.default, - "death_link": DeathLink.default, -} - -allsanity_settings = { - "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_locations, - Goal.internal_name: Goal.default, - FarmType.internal_name: "random", - StartingMoney.internal_name: StartingMoney.default, - ProfitMargin.internal_name: ProfitMargin.default, - BundleRandomization.internal_name: BundleRandomization.default, - BundlePrice.internal_name: BundlePrice.default, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_enabled, - BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - QuestLocations.internal_name: "maximum", - Fishsanity.internal_name: Fishsanity.option_all, - Museumsanity.internal_name: Museumsanity.option_all, - Monstersanity.internal_name: Monstersanity.option_progressive_goals, - Shipsanity.internal_name: Shipsanity.option_everything, - Cooksanity.internal_name: Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_all, - Craftsanity.internal_name: Craftsanity.option_all, - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 1, - NumberOfMovementBuffs.internal_name: 12, - NumberOfLuckBuffs.internal_name: 12, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.default, - MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, - MultipleDaySleepCost.internal_name: MultipleDaySleepCost.default, - ExperienceMultiplier.internal_name: ExperienceMultiplier.default, - FriendshipMultiplier.internal_name: FriendshipMultiplier.default, - DebrisMultiplier.internal_name: DebrisMultiplier.default, - QuickStart.internal_name: QuickStart.default, - Gifting.internal_name: Gifting.default, - "death_link": DeathLink.default, -} - -sv_options_presets: Dict[str, Dict[str, Any]] = { - "All random": all_random_settings, - "Easy": easy_settings, - "Medium": medium_settings, - "Hard": hard_settings, - "Nightmare": nightmare_settings, - "Short": short_settings, - "Minsanity": minsanity_settings, - "Allsanity": allsanity_settings, -} diff --git a/worlds/stardew_valley/region_classes.py b/worlds/stardew_valley/region_classes.py index eaabcfa5fd36..bd64518ea153 100644 --- a/worlds/stardew_valley/region_classes.py +++ b/worlds/stardew_valley/region_classes.py @@ -1,6 +1,7 @@ -from enum import IntFlag -from typing import Optional, List +from copy import deepcopy from dataclasses import dataclass, field +from enum import IntFlag +from typing import Optional, List, Set connector_keyword = " to " @@ -9,15 +10,16 @@ class ModificationFlag(IntFlag): NOT_MODIFIED = 0 MODIFIED = 1 + class RandomizationFlag(IntFlag): NOT_RANDOMIZED = 0b0 - PELICAN_TOWN = 0b11111 - NON_PROGRESSION = 0b11110 - BUILDINGS = 0b11100 - EVERYTHING = 0b11000 - CHAOS = 0b10000 - GINGER_ISLAND = 0b0100000 - LEAD_TO_OPEN_AREA = 0b1000000 + PELICAN_TOWN = 0b00011111 + NON_PROGRESSION = 0b00011110 + BUILDINGS = 0b00011100 + EVERYTHING = 0b00011000 + GINGER_ISLAND = 0b00100000 + LEAD_TO_OPEN_AREA = 0b01000000 + MASTERIES = 0b10000000 @dataclass(frozen=True) @@ -25,6 +27,7 @@ class RegionData: name: str exits: List[str] = field(default_factory=list) flag: ModificationFlag = ModificationFlag.NOT_MODIFIED + is_ginger_island: bool = False def get_merged_with(self, exits: List[str]): merged_exits = [] @@ -32,14 +35,14 @@ def get_merged_with(self, exits: List[str]): if exits is not None: merged_exits.extend(exits) merged_exits = list(set(merged_exits)) - return RegionData(self.name, merged_exits) + return RegionData(self.name, merged_exits, is_ginger_island=self.is_ginger_island) - def get_without_exit(self, exit_to_remove: str): - exits = [exit for exit in self.exits if exit != exit_to_remove] - return RegionData(self.name, exits) + def get_without_exits(self, exits_to_remove: Set[str]): + exits = [exit_ for exit_ in self.exits if exit_ not in exits_to_remove] + return RegionData(self.name, exits, is_ginger_island=self.is_ginger_island) def get_clone(self): - return self.get_merged_with(None) + return deepcopy(self) @dataclass(frozen=True) @@ -62,6 +65,3 @@ class ModRegionData: mod_name: str regions: List[RegionData] connections: List[ConnectionData] - - - diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 4284b438f806..d59439a4879d 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -2,11 +2,12 @@ from typing import Iterable, Dict, Protocol, List, Tuple, Set from BaseClasses import Region, Entrance -from .options import EntranceRandomization, ExcludeGingerIsland, Museumsanity, StardewValleyOptions -from .strings.entrance_names import Entrance -from .strings.region_names import Region -from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag +from .content import content_packs, StardewContent from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod +from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions +from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag +from .strings.entrance_names import Entrance, LogicEntrance +from .strings.region_names import Region, LogicRegion class RegionFactory(Protocol): @@ -17,78 +18,57 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: vanilla_regions = [ RegionData(Region.menu, [Entrance.to_stardew_valley]), RegionData(Region.stardew_valley, [Entrance.to_farmhouse]), - RegionData(Region.farm_house, [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, Entrance.farmhouse_cooking, Entrance.watch_queen_of_sauce]), + RegionData(Region.farm_house, + [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]), RegionData(Region.cellar), - RegionData(Region.kitchen), - RegionData(Region.queen_of_sauce), RegionData(Region.farm, - [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, - Entrance.farm_to_farmcave, Entrance.enter_greenhouse, - Entrance.enter_coop, Entrance.enter_barn, - Entrance.enter_shed, Entrance.enter_slime_hutch, - Entrance.farming, Entrance.shipping]), - RegionData(Region.farming), - RegionData(Region.shipping), + [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse, + Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops, + LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping]), RegionData(Region.backwoods, [Entrance.backwoods_to_mountain]), RegionData(Region.bus_stop, [Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]), RegionData(Region.forest, - [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, - Entrance.forest_to_marnie_ranch, - Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, - Entrance.buy_from_traveling_merchant, - Entrance.attend_flower_dance, Entrance.attend_festival_of_ice]), - RegionData(Region.traveling_cart, [Entrance.buy_from_traveling_merchant_sunday, - Entrance.buy_from_traveling_merchant_monday, - Entrance.buy_from_traveling_merchant_tuesday, - Entrance.buy_from_traveling_merchant_wednesday, - Entrance.buy_from_traveling_merchant_thursday, - Entrance.buy_from_traveling_merchant_friday, - Entrance.buy_from_traveling_merchant_saturday]), - RegionData(Region.traveling_cart_sunday), - RegionData(Region.traveling_cart_monday), - RegionData(Region.traveling_cart_tuesday), - RegionData(Region.traveling_cart_wednesday), - RegionData(Region.traveling_cart_thursday), - RegionData(Region.traveling_cart_friday), - RegionData(Region.traveling_cart_saturday), + [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch, + Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant, + LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby, + LogicEntrance.attend_festival_of_ice]), + RegionData(LogicRegion.forest_waterfall), RegionData(Region.farm_cave), - RegionData(Region.greenhouse), + RegionData(Region.greenhouse, + [LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse, + LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]), RegionData(Region.mountain, [Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop, Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_town, Entrance.mountain_to_maru_room, Entrance.mountain_to_leo_treehouse]), - RegionData(Region.leo_treehouse), + RegionData(Region.leo_treehouse, is_ginger_island=True), RegionData(Region.maru_room), RegionData(Region.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]), RegionData(Region.bus_tunnel), RegionData(Region.town, - [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, - Entrance.town_to_pierre_general_store, Entrance.town_to_saloon, Entrance.town_to_alex_house, - Entrance.town_to_trailer, - Entrance.town_to_mayor_manor, - Entrance.town_to_sam_house, Entrance.town_to_haley_house, Entrance.town_to_sewer, - Entrance.town_to_clint_blacksmith, - Entrance.town_to_museum, - Entrance.town_to_jojamart, Entrance.purchase_movie_ticket, - Entrance.attend_egg_festival, Entrance.attend_fair, Entrance.attend_spirit_eve, Entrance.attend_winter_star]), - RegionData(Region.beach, [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, - Entrance.fishing, - Entrance.attend_luau, Entrance.attend_moonlight_jellies, Entrance.attend_night_market]), - RegionData(Region.fishing), + [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store, + Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house, + Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart, + Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair, + LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]), + RegionData(Region.beach, + [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.fishing, LogicEntrance.attend_luau, + LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]), RegionData(Region.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]), RegionData(Region.ranch), RegionData(Region.leah_house), + RegionData(Region.mastery_cave), RegionData(Region.sewer, [Entrance.enter_mutant_bug_lair]), RegionData(Region.mutant_bug_lair), - RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, - Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), + RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), RegionData(Region.wizard_basement), RegionData(Region.tent), RegionData(Region.carpenter, [Entrance.enter_sebastian_room]), RegionData(Region.sebastian_room), - RegionData(Region.adventurer_guild), + RegionData(Region.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]), + RegionData(Region.adventurer_guild_bedroom), RegionData(Region.community_center, [Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank, Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]), @@ -108,24 +88,21 @@ 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), RegionData(Region.sam_house), RegionData(Region.haley_house), - RegionData(Region.blacksmith, [Entrance.blacksmith_copper]), - RegionData(Region.blacksmith_copper, [Entrance.blacksmith_iron]), - RegionData(Region.blacksmith_iron, [Entrance.blacksmith_gold]), - RegionData(Region.blacksmith_gold, [Entrance.blacksmith_iridium]), - RegionData(Region.blacksmith_iridium), + RegionData(Region.blacksmith, [LogicEntrance.blacksmith_copper]), RegionData(Region.museum), RegionData(Region.jojamart, [Entrance.enter_abandoned_jojamart]), RegionData(Region.abandoned_jojamart, [Entrance.enter_movie_theater]), RegionData(Region.movie_ticket_stand), RegionData(Region.movie_theater), RegionData(Region.fish_shop, [Entrance.fish_shop_to_boat_tunnel]), - RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island]), + RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True), RegionData(Region.elliott_house), RegionData(Region.tide_pools), RegionData(Region.bathhouse_entrance, [Entrance.enter_locker_room]), @@ -138,7 +115,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.quarry_mine_entrance, [Entrance.enter_quarry_mine]), RegionData(Region.quarry_mine), RegionData(Region.secret_woods), - RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis]), + RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]), RegionData(Region.oasis, [Entrance.enter_casino]), RegionData(Region.casino), RegionData(Region.skull_cavern_entrance, [Entrance.enter_skull_cavern]), @@ -151,49 +128,53 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]), RegionData(Region.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]), RegionData(Region.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]), - RegionData(Region.dangerous_skull_cavern), - RegionData(Region.island_south, [Entrance.island_south_to_west, Entrance.island_south_to_north, - Entrance.island_south_to_east, Entrance.island_south_to_southeast, - Entrance.use_island_resort, - Entrance.parrot_express_docks_to_volcano, - Entrance.parrot_express_docks_to_dig_site, - Entrance.parrot_express_docks_to_jungle]), - RegionData(Region.island_resort), + RegionData(Region.dangerous_skull_cavern, is_ginger_island=True), + RegionData(Region.island_south, + [Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast, + Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site, + Entrance.parrot_express_docks_to_jungle], + is_ginger_island=True), + RegionData(Region.island_resort, is_ginger_island=True), RegionData(Region.island_west, - [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, - Entrance.island_west_to_crystals_cave, Entrance.island_west_to_shipwreck, - Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, - Entrance.parrot_express_jungle_to_docks, Entrance.parrot_express_jungle_to_dig_site, - Entrance.parrot_express_jungle_to_volcano]), - RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine]), - RegionData(Region.island_shrine), - RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove]), - RegionData(Region.island_north, [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, - Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano, - Entrance.parrot_express_volcano_to_dig_site, - Entrance.parrot_express_volcano_to_jungle, - Entrance.parrot_express_volcano_to_docks]), - RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach]), - RegionData(Region.volcano_secret_beach), - RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10]), - RegionData(Region.volcano_dwarf_shop), - RegionData(Region.volcano_floor_10), - RegionData(Region.island_trader), - RegionData(Region.island_farmhouse, [Entrance.island_cooking]), - RegionData(Region.gourmand_frog_cave), - RegionData(Region.colored_crystals_cave), - RegionData(Region.shipwreck), - RegionData(Region.qi_walnut_room), - RegionData(Region.leo_hut), - RegionData(Region.pirate_cove), - RegionData(Region.field_office), + [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave, + Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks, + Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island, + LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island, + LogicEntrance.grow_indoor_crops_on_island], + is_ginger_island=True), + RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True), + RegionData(Region.island_shrine, is_ginger_island=True), + RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True), + RegionData(Region.island_north, + [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano, + Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks], + is_ginger_island=True), + RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True), + RegionData(Region.volcano_secret_beach, is_ginger_island=True), + RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True), + RegionData(Region.volcano_dwarf_shop, is_ginger_island=True), + RegionData(Region.volcano_floor_10, is_ginger_island=True), + RegionData(Region.island_trader, is_ginger_island=True), + RegionData(Region.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True), + RegionData(Region.gourmand_frog_cave, is_ginger_island=True), + RegionData(Region.colored_crystals_cave, is_ginger_island=True), + RegionData(Region.shipwreck, is_ginger_island=True), + RegionData(Region.qi_walnut_room, is_ginger_island=True), + RegionData(Region.leo_hut, is_ginger_island=True), + RegionData(Region.pirate_cove, is_ginger_island=True), + RegionData(Region.field_office, is_ginger_island=True), RegionData(Region.dig_site, [Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano, - Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle]), - RegionData(Region.professor_snail_cave), - RegionData(Region.mines, [Entrance.talk_to_mines_dwarf, + Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle], + is_ginger_island=True), + RegionData(Region.professor_snail_cave, is_ginger_island=True), + RegionData(Region.coop), + RegionData(Region.barn), + RegionData(Region.shed), + RegionData(Region.slime_hutch), + + RegionData(Region.mines, [LogicEntrance.talk_to_mines_dwarf, Entrance.dig_to_mines_floor_5]), - RegionData(Region.mines_dwarf_shop), RegionData(Region.mines_floor_5, [Entrance.dig_to_mines_floor_10]), RegionData(Region.mines_floor_10, [Entrance.dig_to_mines_floor_15]), RegionData(Region.mines_floor_15, [Entrance.dig_to_mines_floor_20]), @@ -218,22 +199,59 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.mines_floor_110, [Entrance.dig_to_mines_floor_115]), RegionData(Region.mines_floor_115, [Entrance.dig_to_mines_floor_120]), RegionData(Region.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]), - RegionData(Region.dangerous_mines_20), - RegionData(Region.dangerous_mines_60), - RegionData(Region.dangerous_mines_100), - RegionData(Region.coop), - RegionData(Region.barn), - RegionData(Region.shed), - RegionData(Region.slime_hutch), - RegionData(Region.egg_festival), - RegionData(Region.flower_dance), - RegionData(Region.luau), - RegionData(Region.moonlight_jellies), - RegionData(Region.fair), - RegionData(Region.spirit_eve), - RegionData(Region.festival_of_ice), - RegionData(Region.night_market), - RegionData(Region.winter_star), + RegionData(Region.dangerous_mines_20, is_ginger_island=True), + RegionData(Region.dangerous_mines_60, is_ginger_island=True), + RegionData(Region.dangerous_mines_100, is_ginger_island=True), + + RegionData(LogicRegion.mines_dwarf_shop), + RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]), + RegionData(LogicRegion.blacksmith_iron, [LogicEntrance.blacksmith_gold]), + RegionData(LogicRegion.blacksmith_gold, [LogicEntrance.blacksmith_iridium]), + RegionData(LogicRegion.blacksmith_iridium), + RegionData(LogicRegion.kitchen), + RegionData(LogicRegion.queen_of_sauce), + RegionData(LogicRegion.fishing), + + RegionData(LogicRegion.spring_farming), + RegionData(LogicRegion.summer_farming, [LogicEntrance.grow_summer_fall_crops_in_summer]), + RegionData(LogicRegion.fall_farming, [LogicEntrance.grow_summer_fall_crops_in_fall]), + RegionData(LogicRegion.winter_farming), + RegionData(LogicRegion.summer_or_fall_farming), + RegionData(LogicRegion.indoor_farming), + + RegionData(LogicRegion.shipping), + RegionData(LogicRegion.traveling_cart, [LogicEntrance.buy_from_traveling_merchant_sunday, + LogicEntrance.buy_from_traveling_merchant_monday, + LogicEntrance.buy_from_traveling_merchant_tuesday, + LogicEntrance.buy_from_traveling_merchant_wednesday, + LogicEntrance.buy_from_traveling_merchant_thursday, + LogicEntrance.buy_from_traveling_merchant_friday, + LogicEntrance.buy_from_traveling_merchant_saturday]), + RegionData(LogicRegion.traveling_cart_sunday), + RegionData(LogicRegion.traveling_cart_monday), + RegionData(LogicRegion.traveling_cart_tuesday), + RegionData(LogicRegion.traveling_cart_wednesday), + RegionData(LogicRegion.traveling_cart_thursday), + RegionData(LogicRegion.traveling_cart_friday), + RegionData(LogicRegion.traveling_cart_saturday), + RegionData(LogicRegion.raccoon_daddy, [LogicEntrance.buy_from_raccoon]), + RegionData(LogicRegion.raccoon_shop), + + RegionData(LogicRegion.egg_festival), + RegionData(LogicRegion.desert_festival), + RegionData(LogicRegion.flower_dance), + RegionData(LogicRegion.luau), + RegionData(LogicRegion.trout_derby), + RegionData(LogicRegion.moonlight_jellies), + RegionData(LogicRegion.fair), + RegionData(LogicRegion.spirit_eve), + RegionData(LogicRegion.festival_of_ice), + RegionData(LogicRegion.night_market), + RegionData(LogicRegion.winter_star), + RegionData(LogicRegion.squidfest), + RegionData(LogicRegion.bookseller_1, [LogicEntrance.buy_year1_books]), + RegionData(LogicRegion.bookseller_2, [LogicEntrance.buy_year3_books]), + RegionData(LogicRegion.bookseller_3), ] # Exists and where they lead @@ -242,19 +260,15 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.to_farmhouse, Region.farm_house), ConnectionData(Entrance.farmhouse_to_farm, Region.farm), ConnectionData(Entrance.downstairs_to_cellar, Region.cellar), - ConnectionData(Entrance.farmhouse_cooking, Region.kitchen), - ConnectionData(Entrance.watch_queen_of_sauce, Region.queen_of_sauce), ConnectionData(Entrance.farm_to_backwoods, Region.backwoods), ConnectionData(Entrance.farm_to_bus_stop, Region.bus_stop), ConnectionData(Entrance.farm_to_forest, Region.forest), ConnectionData(Entrance.farm_to_farmcave, Region.farm_cave, flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData(Entrance.farming, Region.farming), ConnectionData(Entrance.enter_greenhouse, Region.greenhouse), ConnectionData(Entrance.enter_coop, Region.coop), ConnectionData(Entrance.enter_barn, Region.barn), ConnectionData(Entrance.enter_shed, Region.shed), ConnectionData(Entrance.enter_slime_hutch, Region.slime_hutch), - ConnectionData(Entrance.shipping, Region.shipping), ConnectionData(Entrance.use_desert_obelisk, Region.desert), ConnectionData(Entrance.use_island_obelisk, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.use_farm_obelisk, Region.farm), @@ -273,14 +287,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.enter_secret_woods, Region.secret_woods), ConnectionData(Entrance.forest_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.buy_from_traveling_merchant, Region.traveling_cart), - ConnectionData(Entrance.buy_from_traveling_merchant_sunday, Region.traveling_cart_sunday), - ConnectionData(Entrance.buy_from_traveling_merchant_monday, Region.traveling_cart_monday), - ConnectionData(Entrance.buy_from_traveling_merchant_tuesday, Region.traveling_cart_tuesday), - ConnectionData(Entrance.buy_from_traveling_merchant_wednesday, Region.traveling_cart_wednesday), - ConnectionData(Entrance.buy_from_traveling_merchant_thursday, Region.traveling_cart_thursday), - ConnectionData(Entrance.buy_from_traveling_merchant_friday, Region.traveling_cart_friday), - ConnectionData(Entrance.buy_from_traveling_merchant_saturday, Region.traveling_cart_saturday), + ConnectionData(Entrance.forest_to_mastery_cave, Region.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES), ConnectionData(Entrance.town_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.enter_mutant_bug_lair, Region.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.mountain_to_railroad, Region.railroad), @@ -295,6 +302,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.enter_sebastian_room, Region.sebastian_room, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(Entrance.adventurer_guild_to_bedroom, Region.adventurer_guild_bedroom), ConnectionData(Entrance.enter_quarry, Region.quarry), ConnectionData(Entrance.enter_quarry_mine_entrance, Region.quarry_mine_entrance, flag=RandomizationFlag.BUILDINGS), @@ -316,10 +324,6 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.enter_sunroom, Region.sunroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.town_to_clint_blacksmith, Region.blacksmith, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.blacksmith_copper, Region.blacksmith_copper), - ConnectionData(Entrance.blacksmith_iron, Region.blacksmith_iron), - ConnectionData(Entrance.blacksmith_gold, Region.blacksmith_gold), - ConnectionData(Entrance.blacksmith_iridium, Region.blacksmith_iridium), ConnectionData(Entrance.town_to_saloon, Region.saloon, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.play_journey_of_the_prairie_king, Region.jotpk_world_1), @@ -328,6 +332,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, @@ -354,10 +359,8 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.boat_to_ginger_island, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.enter_tide_pools, Region.tide_pools), - ConnectionData(Entrance.fishing, Region.fishing), ConnectionData(Entrance.mountain_to_the_mines, Region.mines, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.talk_to_mines_dwarf, Region.mines_dwarf_shop), ConnectionData(Entrance.dig_to_mines_floor_5, Region.mines_floor_5), ConnectionData(Entrance.dig_to_mines_floor_10, Region.mines_floor_10), ConnectionData(Entrance.dig_to_mines_floor_15, Region.mines_floor_15), @@ -416,7 +419,6 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.use_island_resort, Region.island_resort, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.island_west_to_islandfarmhouse, Region.island_farmhouse, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_cooking, Region.kitchen), ConnectionData(Entrance.island_west_to_gourmand_cave, Region.gourmand_frog_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.island_west_to_crystals_cave, Region.colored_crystals_cave, @@ -454,15 +456,62 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.attend_egg_festival, Region.egg_festival), - ConnectionData(Entrance.attend_flower_dance, Region.flower_dance), - ConnectionData(Entrance.attend_luau, Region.luau), - ConnectionData(Entrance.attend_moonlight_jellies, Region.moonlight_jellies), - ConnectionData(Entrance.attend_fair, Region.fair), - ConnectionData(Entrance.attend_spirit_eve, Region.spirit_eve), - ConnectionData(Entrance.attend_festival_of_ice, Region.festival_of_ice), - ConnectionData(Entrance.attend_night_market, Region.night_market), - ConnectionData(Entrance.attend_winter_star, Region.winter_star), + + ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop), + + ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday), + ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy), + ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall), + ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop), + ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen), + ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce), + + ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming), + ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming), + ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming), + ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming), + ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming), + ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming), + ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming), + ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming), + ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming), + ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming), + ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming), + + ConnectionData(LogicEntrance.shipping, LogicRegion.shipping), + ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper), + ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron), + ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold), + ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium), + ConnectionData(LogicEntrance.fishing, LogicRegion.fishing), + ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen), + ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival), + ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival), + ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance), + ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau), + ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby), + ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies), + ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair), + ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve), + ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice), + ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market), + ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star), + ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest), + ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1), + ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2), + ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3), ] @@ -491,7 +540,7 @@ def create_final_regions(world_options) -> List[RegionData]: def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, ConnectionData], Dict[str, RegionData]]: regions_data: Dict[str, RegionData] = {region.name: region for region in create_final_regions(world_options)} connections = {connection.name: connection for connection in vanilla_connections} - connections = modify_connections_for_mods(connections, world_options.mods) + connections = modify_connections_for_mods(connections, sorted(world_options.mods.value)) include_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_false return remove_ginger_island_regions_and_connections(regions_data, connections, include_island) @@ -499,19 +548,27 @@ def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, Conne def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, RegionData], connections: Dict[str, ConnectionData], include_island: bool): if include_island: return connections, regions_by_name - for connection_name in list(connections): + + removed_connections = set() + + for connection_name in tuple(connections): connection = connections[connection_name] if connection.flag & RandomizationFlag.GINGER_ISLAND: - regions_by_name.pop(connection.destination, None) connections.pop(connection_name) - regions_by_name = {name: regions_by_name[name].get_without_exit(connection_name) for name in regions_by_name} + removed_connections.add(connection_name) + + for region_name in tuple(regions_by_name): + region = regions_by_name[region_name] + if region.is_ginger_island: + regions_by_name.pop(region_name) + else: + regions_by_name[region_name] = region.get_without_exits(removed_connections) + return connections, regions_by_name -def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods) -> Dict[str, ConnectionData]: - if mods is None: - return connections - for mod in mods.value: +def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods: Iterable) -> Dict[str, ConnectionData]: + for mod in mods: if mod not in ModDataList: continue if mod in vanilla_connections_to_remove_by_mod: @@ -522,7 +579,6 @@ def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods) -> def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionData) -> RegionData: - updated_region = existing_region region_exits = updated_region.exits modified_exits = modified_region.exits @@ -532,14 +588,18 @@ def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionD return updated_region -def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) -> Tuple[ - Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: +def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions, content: StardewContent) \ + -> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: entrances_data, regions_data = create_final_connections_and_regions(world_options) regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data} - entrances_by_name: Dict[str: Entrance] = {entrance.name: entrance for region in regions_by_name.values() for entrance in region.exits - if entrance.name in entrances_data} + entrances_by_name: Dict[str: Entrance] = { + entrance.name: entrance + for region in regions_by_name.values() + for entrance in region.exits + if entrance.name in entrances_data + } - connections, randomized_data = randomize_connections(random, world_options, regions_data, entrances_data) + connections, randomized_data = randomize_connections(random, world_options, content, regions_data, entrances_data) for connection in connections: if connection.name in entrances_by_name: @@ -547,7 +607,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options: return regions_by_name, entrances_by_name, randomized_data -def randomize_connections(random: Random, world_options: StardewValleyOptions, regions_by_name: Dict[str, RegionData], +def randomize_connections(random: Random, world_options: StardewValleyOptions, content: StardewContent, regions_by_name: Dict[str, RegionData], connections_by_name: Dict[str, ConnectionData]) -> Tuple[List[ConnectionData], Dict[str, str]]: connections_to_randomize: List[ConnectionData] = [] if world_options.entrance_randomization == EntranceRandomization.option_pelican_town: @@ -556,13 +616,13 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r elif world_options.entrance_randomization == EntranceRandomization.option_non_progression: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.NON_PROGRESSION in connections_by_name[connection].flag] - elif world_options.entrance_randomization == EntranceRandomization.option_buildings: + elif world_options.entrance_randomization == EntranceRandomization.option_buildings or world_options.entrance_randomization == EntranceRandomization.option_buildings_without_house: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.BUILDINGS in connections_by_name[connection].flag] elif world_options.entrance_randomization == EntranceRandomization.option_chaos: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.BUILDINGS in connections_by_name[connection].flag] - connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options) + connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content) # On Chaos, we just add the connections to randomize, unshuffled, and the client does it every day randomized_data_for_mod = {} @@ -571,7 +631,7 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r randomized_data_for_mod[connection.reverse] = connection.reverse return list(connections_by_name.values()), randomized_data_for_mod - connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options) + connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content) random.shuffle(connections_to_randomize) destination_pool = list(connections_to_randomize) random.shuffle(destination_pool) @@ -586,10 +646,12 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r return randomized_connections_for_generation, randomized_data_for_mod -def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], world_options: StardewValleyOptions) -> List[ConnectionData]: - exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true - if exclude_island: +def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], content: StardewContent) -> List[ConnectionData]: + # FIXME remove when regions are handled in content packs + if content_packs.ginger_island_content_pack.name not in content.registered_packs: connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] + if not content.features.skill_progression.are_masteries_shuffled: + connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag] return connections_to_randomize @@ -685,7 +747,7 @@ def swap_one_random_connection(regions_by_name, connections_by_name, randomized_ for connection in randomized_connections if connection != randomized_connections[connection]} unreachable_regions_names_leading_somewhere = tuple([region for region in unreachable_regions - if len(regions_by_name[region].exits) > 0]) + if len(regions_by_name[region].exits) > 0]) unreachable_regions_leading_somewhere = [regions_by_name[region_name] for region_name in unreachable_regions_names_leading_somewhere] unreachable_regions_exits_names = [exit_name for region in unreachable_regions_leading_somewhere for exit_name in region.exits] unreachable_connections = [connections_by_name[exit_name] for exit_name in unreachable_regions_exits_names] diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt deleted file mode 100644 index b0922176e43b..000000000000 --- a/worlds/stardew_valley/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -importlib_resources; python_version <= '3.8' diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 8002031ac792..54afc31eb892 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1,11 +1,16 @@ import itertools +import logging from typing import List, Dict, Set -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from worlds.generic import Rules as MultiWorldRules from . import locations from .bundles.bundle_room import BundleRoom +from .content import StardewContent +from .content.feature import friendsanity from .data.craftable_data import all_crafting_recipes_by_name +from .data.game_item import ItemTag +from .data.harvest import HarvestCropSource, HarvestFruitTreeSource from .data.museum_data import all_museum_items, dwarf_scrolls, skeleton_front, skeleton_middle, skeleton_back, all_museum_items_by_name, all_museum_minerals, \ all_museum_artifacts, Artifact from .data.recipe_data import all_cooking_recipes_by_name @@ -14,39 +19,47 @@ from .logic.time_logic import MAX_MONTHS from .logic.tool_logic import tool_upgrade_prices from .mods.mod_data import ModNames -from .options import StardewValleyOptions, Friendsanity +from .options import StardewValleyOptions, Walnutsanity from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ - Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, Cropsanity, SkillProgression -from .stardew_rule import And, StardewRule + Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity +from .stardew_rule import And, StardewRule, true_ from .stardew_rule.indirect_connection import look_for_indirect_connection -from .strings.ap_names.event_names import Event +from .stardew_rule.rule_explain import explain +from .strings.ap_names.ap_option_names import WalnutsanityOptionName +from .strings.ap_names.community_upgrade_names import CommunityUpgrade from .strings.ap_names.mods.mod_items import SVEQuestItem, SVERunes from .strings.ap_names.transport_names import Transportation from .strings.artisan_good_names import ArtisanGood from .strings.building_names import Building from .strings.bundle_names import CCRoom from .strings.calendar_names import Weekday -from .strings.craftable_names import Bomb -from .strings.crop_names import Fruit +from .strings.craftable_names import Bomb, Furniture +from .strings.crop_names import Fruit, Vegetable from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \ - SVEEntrance, LaceyEntrance, BoardingHouseEntrance + 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 +from .strings.metal_names import MetalBar, Mineral +from .strings.monster_names import Monster from .strings.performance_names import Performance from .strings.quest_names import Quest from .strings.region_names import Region from .strings.season_names import Season -from .strings.skill_names import ModSkill, Skill +from .strings.skill_names import Skill from .strings.tool_names import Tool, ToolMaterial from .strings.tv_channel_names import Channel from .strings.villager_names import NPC, ModNPC from .strings.wallet_item_names import Wallet +logger = logging.getLogger(__name__) + def set_rules(world): multiworld = world.multiworld world_options = world.options + world_content = world.content player = world.player logic = world.logic bundle_rooms: List[BundleRoom] = world.modified_bundles @@ -57,17 +70,17 @@ def set_rules(world): set_ginger_island_rules(logic, multiworld, player, world_options) set_tool_rules(logic, multiworld, player, world_options) - set_skills_rules(logic, multiworld, player, world_options) - set_bundle_rules(bundle_rooms, logic, multiworld, player) + set_skills_rules(logic, multiworld, player, world_content) + set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options) set_building_rules(logic, multiworld, player, world_options) - set_cropsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_cropsanity_rules(logic, multiworld, player, world_content) set_story_quests_rules(all_location_names, logic, multiworld, player, world_options) set_special_order_rules(all_location_names, logic, multiworld, player, world_options) set_help_wanted_quests_rules(logic, multiworld, player, world_options) set_fishsanity_rules(all_location_names, logic, multiworld, player) set_museumsanity_rules(all_location_names, logic, multiworld, player, world_options) - set_friendsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_friendsanity_rules(logic, multiworld, player, world_content) set_backpack_rules(logic, multiworld, player, world_options) set_festival_rules(all_location_names, logic, multiworld, player) set_monstersanity_rules(all_location_names, logic, multiworld, player, world_options) @@ -75,6 +88,7 @@ def set_rules(world): set_cooksanity_rules(all_location_names, logic, multiworld, player, world_options) set_chefsanity_rules(all_location_names, logic, multiworld, player, world_options) set_craftsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_booksanity_rules(logic, multiworld, player, world_content) set_isolated_locations_rules(logic, multiworld, player) set_traveling_merchant_day_rules(logic, multiworld, player) set_arcade_machine_rules(logic, multiworld, player, world_options) @@ -93,6 +107,8 @@ def set_isolated_locations_rules(logic: StardewLogic, multiworld, player): logic.money.can_spend(20000)) MultiWorldRules.add_rule(multiworld.get_location("Demetrius's Breakthrough", player), logic.money.can_have_earned_total(25000)) + MultiWorldRules.add_rule(multiworld.get_location("Pot Of Gold", player), + logic.season.has(Season.spring)) def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -104,8 +120,10 @@ def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: Stard MultiWorldRules.add_rule(multiworld.get_location("Purchase Iridium Rod", player), (logic.skill.has_level(Skill.fishing, 6) & logic.money.can_spend(7500))) + MultiWorldRules.add_rule(multiworld.get_location("Copper Pan Cutscene", player), logic.received("Glittering Boulder Removed")) + materials = [None, "Copper", "Iron", "Gold", "Iridium"] - tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.watering_can, Tool.trash_can] + tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can, Tool.pan] for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool): if previous is None: continue @@ -124,72 +142,50 @@ def set_building_rules(logic: StardewLogic, multiworld, player, world_options: S logic.registry.building_rules[building.name.replace(" Blueprint", "")]) -def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player): +def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): for bundle_room in bundle_rooms: room_rules = [] for bundle in bundle_room.bundles: location = multiworld.get_location(bundle.name, player) bundle_rules = logic.bundle.can_complete_bundle(bundle) + if bundle_room.name == CCRoom.raccoon_requests: + num = int(bundle.name[-1]) + extra_raccoons = 1 if world_options.quest_locations >= 0 else 0 + 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}" + bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name) room_rules.append(bundle_rules) MultiWorldRules.set_rule(location, bundle_rules) - if bundle_room.name == CCRoom.abandoned_joja_mart: + if bundle_room.name == CCRoom.abandoned_joja_mart or bundle_room.name == CCRoom.raccoon_requests: continue room_location = f"Complete {bundle_room.name}" MultiWorldRules.add_rule(multiworld.get_location(room_location, player), And(*room_rules)) -def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - mods = world_options.mods - if world_options.skill_progression == SkillProgression.option_vanilla: +def set_skills_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, content: StardewContent): + skill_progression = content.features.skill_progression + if not skill_progression.is_progressive: 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) - - -def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): - set_vanilla_skill_rule(logic, multiworld, player, Skill.farming, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.fishing, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.foraging, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.mining, level) - set_vanilla_skill_rule(logic, multiworld, player, Skill.combat, level) - - -def set_modded_skill_rule_for_level(logic: StardewLogic, multiworld, player, mods, level: int): - if ModNames.luck_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.luck, level) - if ModNames.magic in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.magic, level) - if ModNames.binning_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.binning, level) - if ModNames.cooking_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.cooking, level) - if ModNames.socializing_skill in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.socializing, level) - if ModNames.archaeology in mods: - set_modded_skill_rule(logic, multiworld, player, ModSkill.archaeology, level) - - -def get_skill_level_location(multiworld, player, skill: str, level: int): - location_name = f"Level {level} {skill}" - return multiworld.get_location(location_name, player) - - -def set_vanilla_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int): - rule = logic.skill.can_earn_level(skill, level) - MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule) + for skill in content.skills.values(): + for level, level_name in skill_progression.get_randomized_level_names_by_level(skill): + rule = logic.skill.can_earn_level(skill.name, level) + location = multiworld.get_location(level_name, player) + MultiWorldRules.set_rule(location, rule) -def set_modded_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int): - rule = logic.mod.skill.can_earn_mod_skill_level(skill, level) - MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule) + if skill_progression.is_mastery_randomized(skill): + rule = logic.skill.can_earn_mastery(skill.name) + location = multiworld.get_location(skill.mastery_name, player) + MultiWorldRules.set_rule(location, rule) def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): set_mines_floor_entrance_rules(logic, multiworld, player) set_skull_cavern_floor_entrance_rules(logic, multiworld, player) set_blacksmith_entrance_rules(logic, multiworld, player) - set_skill_entrance_rules(logic, multiworld, player) + set_skill_entrance_rules(logic, multiworld, player, world_options) set_traveling_merchant_day_rules(logic, multiworld, player) set_dangerous_mine_rules(logic, multiworld, player, world_options) @@ -204,8 +200,12 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, Entrance.purchase_movie_ticket, movie_theater_rule) set_entrance_rule(multiworld, player, Entrance.take_bus_to_desert, logic.received("Bus Repair")) set_entrance_rule(multiworld, player, Entrance.enter_skull_cavern, logic.received(Wallet.skull_key)) - set_entrance_rule(multiworld, player, Entrance.talk_to_mines_dwarf, logic.wallet.can_speak_dwarf() & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron)) - set_entrance_rule(multiworld, player, Entrance.buy_from_traveling_merchant, logic.traveling_merchant.has_days()) + set_entrance_rule(multiworld, player, LogicEntrance.talk_to_mines_dwarf, + logic.wallet.can_speak_dwarf() & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron)) + set_entrance_rule(multiworld, player, LogicEntrance.buy_from_traveling_merchant, logic.traveling_merchant.has_days()) + set_entrance_rule(multiworld, player, LogicEntrance.buy_from_raccoon, logic.quest.has_raccoon_shop()) + set_entrance_rule(multiworld, player, LogicEntrance.fish_in_waterfall, + logic.skill.has_level(Skill.fishing, 5) & logic.tool.has_fishing_rod(2)) set_farm_buildings_entrance_rules(logic, multiworld, player) @@ -213,15 +213,21 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, Entrance.enter_witch_warp_cave, logic.quest.has_dark_talisman() | (logic.mod.magic.can_blink())) set_entrance_rule(multiworld, player, Entrance.enter_witch_hut, (logic.has(ArtisanGood.void_mayonnaise) | logic.mod.magic.can_blink())) set_entrance_rule(multiworld, player, Entrance.enter_mutant_bug_lair, - (logic.received(Event.start_dark_talisman_quest) & logic.relationship.can_meet(NPC.krobus)) | logic.mod.magic.can_blink()) + (logic.wallet.has_rusty_key() & logic.region.can_reach(Region.railroad) & logic.relationship.can_meet( + NPC.krobus)) | logic.mod.magic.can_blink()) set_entrance_rule(multiworld, player, Entrance.enter_casino, logic.quest.has_club_card()) set_bedroom_entrance_rules(logic, multiworld, player, world_options) set_festival_entrance_rules(logic, multiworld, player) - set_island_entrance_rule(multiworld, player, Entrance.island_cooking, logic.cooking.can_cook_in_kitchen, world_options) - set_entrance_rule(multiworld, player, Entrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen) - set_entrance_rule(multiworld, player, Entrance.shipping, logic.shipping.can_use_shipping_bin) - set_entrance_rule(multiworld, player, Entrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce)) + set_island_entrance_rule(multiworld, player, LogicEntrance.island_cooking, logic.cooking.can_cook_in_kitchen, world_options) + 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, 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): @@ -236,6 +242,7 @@ def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewVa def set_farm_buildings_entrance_rules(logic, multiworld, player): + set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_house(3)) set_entrance_rule(multiworld, player, Entrance.use_desert_obelisk, logic.can_use_obelisk(Transportation.desert_obelisk)) set_entrance_rule(multiworld, player, Entrance.enter_greenhouse, logic.received("Greenhouse")) set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop)) @@ -263,8 +270,7 @@ def set_mines_floor_entrance_rules(logic, multiworld, player): rule = logic.mine.has_mine_elevator_to_floor(floor - 10) if floor == 5 or floor == 45 or floor == 85: rule = rule & logic.mine.can_progress_in_the_mines_from_floor(floor) - entrance = multiworld.get_entrance(dig_to_mines_floor(floor), player) - MultiWorldRules.set_rule(entrance, rule) + set_entrance_rule(multiworld, player, dig_to_mines_floor(floor), rule) def set_skull_cavern_floor_entrance_rules(logic, multiworld, player): @@ -272,41 +278,55 @@ def set_skull_cavern_floor_entrance_rules(logic, multiworld, player): rule = logic.mod.elevator.has_skull_cavern_elevator_to_floor(floor - 25) if floor == 25 or floor == 75 or floor == 125: rule = rule & logic.mine.can_progress_in_the_skull_cavern_from_floor(floor) - entrance = multiworld.get_entrance(dig_to_skull_floor(floor), player) - MultiWorldRules.set_rule(entrance, rule) + set_entrance_rule(multiworld, player, dig_to_skull_floor(floor), rule) def set_blacksmith_entrance_rules(logic, multiworld, player): - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium) - - -def set_skill_entrance_rules(logic, multiworld, player): - set_entrance_rule(multiworld, player, Entrance.farming, logic.skill.can_get_farming_xp) - set_entrance_rule(multiworld, player, Entrance.fishing, logic.skill.can_get_fishing_xp) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium) + + +def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions): + set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops, logic.farming.has_farming_tools & logic.season.has_spring) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops, logic.farming.has_farming_tools & logic.season.has_summer) + set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops, logic.farming.has_farming_tools & logic.season.has_fall) + set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_in_greenhouse, logic.farming.has_farming_tools) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_on_island, logic.farming.has_farming_tools, world_options) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_summer, true_) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_fall, true_) + + set_entrance_rule(multiworld, player, LogicEntrance.fishing, logic.skill.can_get_fishing_xp) def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str): - material_entrance = multiworld.get_entrance(entrance_name, player) upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material]) - MultiWorldRules.set_rule(material_entrance, upgrade_rule) + set_entrance_rule(multiworld, player, entrance_name, upgrade_rule) def set_festival_entrance_rules(logic, multiworld, player): - set_entrance_rule(multiworld, player, Entrance.attend_egg_festival, logic.season.has(Season.spring)) - set_entrance_rule(multiworld, player, Entrance.attend_flower_dance, logic.season.has(Season.spring)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_egg_festival, logic.season.has(Season.spring)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_desert_festival, logic.season.has(Season.spring) & logic.received("Bus Repair")) + set_entrance_rule(multiworld, player, LogicEntrance.attend_flower_dance, logic.season.has(Season.spring)) - set_entrance_rule(multiworld, player, Entrance.attend_luau, logic.season.has(Season.summer)) - set_entrance_rule(multiworld, player, Entrance.attend_moonlight_jellies, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_luau, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_trout_derby, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_moonlight_jellies, logic.season.has(Season.summer)) - set_entrance_rule(multiworld, player, Entrance.attend_fair, logic.season.has(Season.fall)) - set_entrance_rule(multiworld, player, Entrance.attend_spirit_eve, logic.season.has(Season.fall)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_fair, logic.season.has(Season.fall)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_spirit_eve, logic.season.has(Season.fall)) - set_entrance_rule(multiworld, player, Entrance.attend_festival_of_ice, logic.season.has(Season.winter)) - set_entrance_rule(multiworld, player, Entrance.attend_night_market, logic.season.has(Season.winter)) - set_entrance_rule(multiworld, player, Entrance.attend_winter_star, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_festival_of_ice, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_squidfest, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_night_market, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_winter_star, logic.season.has(Season.winter)) def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -319,7 +339,8 @@ def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_optio MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player), logic.has(Bomb.cherry_bomb)) MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), - logic.can_complete_field_office()) + logic.walnut.can_complete_field_office()) + set_walnut_rules(logic, multiworld, player, world_options) def set_boat_repair_rules(logic: StardewLogic, multiworld, player): @@ -374,10 +395,11 @@ def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_op def set_island_parrot_rules(logic: StardewLogic, multiworld, player): - has_walnut = logic.has_walnut(1) - has_5_walnut = logic.has_walnut(5) - has_10_walnut = logic.has_walnut(10) - has_20_walnut = logic.has_walnut(20) + # Logic rules require more walnuts than in reality, to allow the player to spend them "wrong" + has_walnut = logic.walnut.has_walnut(5) + has_5_walnut = logic.walnut.has_walnut(15) + has_10_walnut = logic.walnut.has_walnut(40) + has_20_walnut = logic.walnut.has_walnut(60) MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player), has_walnut) MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player), @@ -403,17 +425,84 @@ def set_island_parrot_rules(logic: StardewLogic, multiworld, player): has_10_walnut) -def set_cropsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - if world_options.cropsanity == Cropsanity.option_disabled: +def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): + if world_options.walnutsanity == Walnutsanity.preset_none: + return + + set_walnut_puzzle_rules(logic, multiworld, player, world_options) + set_walnut_bushes_rules(logic, multiworld, player, world_options) + set_walnut_dig_spot_rules(logic, multiworld, player, world_options) + set_walnut_repeatable_rules(logic, multiworld, player, world_options) + + +def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_options): + if WalnutsanityOptionName.puzzles not in world_options.walnutsanity: + return + + MultiWorldRules.add_rule(multiworld.get_location("Open Golden Coconut", player), logic.has(Geode.golden_coconut)) + MultiWorldRules.add_rule(multiworld.get_location("Banana Altar", player), logic.has(Fruit.banana)) + MultiWorldRules.add_rule(multiworld.get_location("Leo's Tree", player), logic.tool.has_tool(Tool.axe)) + MultiWorldRules.add_rule(multiworld.get_location("Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) & + logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) & + logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south))) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon")) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) + MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) + MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot) + MultiWorldRules.add_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) + MultiWorldRules.add_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block)) + + +def set_walnut_bushes_rules(logic, multiworld, player, world_options): + if WalnutsanityOptionName.bushes not in world_options.walnutsanity: + return + # I don't think any of the bushes require something special, but that might change with ER + return + + +def set_walnut_dig_spot_rules(logic, multiworld, player, world_options): + if WalnutsanityOptionName.dig_spots not in world_options.walnutsanity: + return + + for dig_spot_walnut in locations.locations_by_tag[LocationTags.WALNUTSANITY_DIG]: + rule = logic.tool.has_tool(Tool.hoe) + if "Journal Scrap" in dig_spot_walnut.name: + rule = rule & logic.has(Forageable.journal_scrap) + if "Starfish Diamond" in dig_spot_walnut.name: + rule = rule & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) + MultiWorldRules.set_rule(multiworld.get_location(dig_spot_walnut.name, player), rule) + + +def set_walnut_repeatable_rules(logic, multiworld, player, world_options): + if WalnutsanityOptionName.repeatables not in world_options.walnutsanity: + return + for i in range(1, 6): + MultiWorldRules.set_rule(multiworld.get_location(f"Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1)) + MultiWorldRules.set_rule(multiworld.get_location(f"Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp) + MultiWorldRules.set_rule(multiworld.get_location(f"Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon) + MultiWorldRules.set_rule(multiworld.get_location(f"Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime)) + + +def set_cropsanity_rules(logic: StardewLogic, multiworld, player, world_content: StardewContent): + if not world_content.features.cropsanity.is_enabled: return - harvest_prefix = "Harvest " - harvest_prefix_length = len(harvest_prefix) - for harvest_location in locations.locations_by_tag[LocationTags.CROPSANITY]: - if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options.mods): - crop_name = harvest_location.name[harvest_prefix_length:] - MultiWorldRules.set_rule(multiworld.get_location(harvest_location.name, player), - logic.has(crop_name)) + for item in world_content.find_tagged_items(ItemTag.CROPSANITY): + location = world_content.features.cropsanity.to_location_name(item.name) + harvest_sources = (source for source in item.sources if isinstance(source, (HarvestFruitTreeSource, HarvestCropSource))) + MultiWorldRules.set_rule(multiworld.get_location(location, player), logic.source.has_access_to_any(harvest_sources)) def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -427,23 +516,21 @@ def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, mu def set_special_order_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - if world_options.special_order_locations == SpecialOrderLocations.option_disabled: - return - board_rule = logic.received("Special Order Board") & logic.time.has_lived_months(4) - for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_order.name in all_location_names: - order_rule = board_rule & logic.registry.special_order_rules[board_order.name] - MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule) + if world_options.special_order_locations & SpecialOrderLocations.option_board: + board_rule = logic.received("Special Order Board") & logic.time.has_lived_months(4) + for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_order.name in all_location_names: + order_rule = board_rule & logic.registry.special_order_rules[board_order.name] + MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule) if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - if world_options.special_order_locations == SpecialOrderLocations.option_board_only: - return - qi_rule = logic.region.can_reach(Region.qi_walnut_room) & logic.time.has_lived_months(8) - for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: - if qi_order.name in all_location_names: - order_rule = qi_rule & logic.registry.special_order_rules[qi_order.name] - MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule) + if world_options.special_order_locations & SpecialOrderLocations.value_qi: + qi_rule = logic.region.can_reach(Region.qi_walnut_room) & logic.time.has_lived_months(8) + for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: + if qi_order.name in all_location_names: + order_rule = qi_rule & logic.registry.special_order_rules[qi_order.name] + MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule) help_wanted_prefix = "Help Wanted:" @@ -730,6 +817,21 @@ def set_craftsanity_rules(all_location_names: Set[str], logic: StardewLogic, mul MultiWorldRules.set_rule(multiworld.get_location(location.name, player), craft_rule) +def set_booksanity_rules(logic: StardewLogic, multiworld, player, content: StardewContent): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + for book in content.find_tagged_items(ItemTag.BOOK): + if booksanity.is_included(book): + MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book.name), player), logic.has(book.name)) + + for i, book in enumerate(booksanity.get_randomized_lost_books()): + if i <= 0: + continue + MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book), player), logic.received(booksanity.progressive_lost_book, i)) + + def set_traveling_merchant_day_rules(logic: StardewLogic, multiworld: MultiWorld, player: int): for day in Weekday.all_days: item_for_day = f"Traveling Merchant: {day}" @@ -738,51 +840,43 @@ def set_traveling_merchant_day_rules(logic: StardewLogic, multiworld: MultiWorld def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player), - logic.received(Wallet.skull_key)) + play_junimo_kart_rule = logic.received(Wallet.skull_key) + if world_options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling: + set_entrance_rule(multiworld, player, Entrance.play_junimo_kart, play_junimo_kart_rule) return - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player), - logic.has("Junimo Kart Small Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_2, 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), - 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")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_2, player), - logic.has("JotPK Medium Buff")) - MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_3, player), - logic.has("JotPK Big Buff")) + set_entrance_rule(multiworld, player, Entrance.play_junimo_kart, play_junimo_kart_rule & logic.has("Junimo Kart Small Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_2, logic.has("Junimo Kart Medium Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_3, logic.has("Junimo Kart Big Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_4, logic.has("Junimo Kart Max Buff")) + set_entrance_rule(multiworld, player, Entrance.play_journey_of_the_prairie_king, logic.has("JotPK Small Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_jotpk_world_2, logic.has("JotPK Medium Buff")) + set_entrance_rule(multiworld, player, Entrance.reach_jotpk_world_3, logic.has("JotPK Big Buff")) MultiWorldRules.add_rule(multiworld.get_location("Journey of the Prairie King Victory", player), logic.has("JotPK Max Buff")) -def set_friendsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): - if world_options.friendsanity == Friendsanity.option_none: +def set_friendsanity_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, content: StardewContent): + if not content.features.friendsanity.is_enabled: return MultiWorldRules.add_rule(multiworld.get_location("Spouse Stardrop", player), - logic.relationship.has_hearts(Generic.bachelor, 13)) + logic.relationship.has_hearts_with_any_bachelor(13)) MultiWorldRules.add_rule(multiworld.get_location("Have a Baby", player), logic.relationship.can_reproduce(1)) MultiWorldRules.add_rule(multiworld.get_location("Have Another Baby", player), logic.relationship.can_reproduce(2)) - friend_prefix = "Friendsanity: " - friend_suffix = " <3" - for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]: - if not friend_location.name in all_location_names: - continue - friend_location_without_prefix = friend_location.name[len(friend_prefix):] - friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)] - split_index = friend_location_trimmed.rindex(" ") - friend_name = friend_location_trimmed[:split_index] - num_hearts = int(friend_location_trimmed[split_index + 1:]) - MultiWorldRules.set_rule(multiworld.get_location(friend_location.name, player), - logic.relationship.can_earn_relationship(friend_name, num_hearts)) + for villager in content.villagers.values(): + for heart in content.features.friendsanity.get_randomized_hearts(villager): + rule = logic.relationship.can_earn_relationship(villager.name, heart) + location_name = friendsanity.to_location_name(villager.name, heart) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule) + + for heart in content.features.friendsanity.get_pet_randomized_hearts(): + rule = logic.pet.can_befriend_pet(heart) + location_name = friendsanity.to_location_name(NPC.pet, heart) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule) def set_deepwoods_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): @@ -876,7 +970,7 @@ def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, worl set_entrance_rule(multiworld, player, SVEEntrance.outpost_warp_to_outpost, logic.received(SVERunes.nexus_outpost)) set_entrance_rule(multiworld, player, SVEEntrance.wizard_warp_to_wizard, logic.received(SVERunes.nexus_wizard)) set_entrance_rule(multiworld, player, SVEEntrance.use_purple_junimo, logic.relationship.has_hearts(ModNPC.apples, 10)) - set_entrance_rule(multiworld, player, SVEEntrance.grandpa_interior_to_upstairs, logic.received(SVEQuestItem.grandpa_shed)) + set_entrance_rule(multiworld, player, SVEEntrance.grandpa_interior_to_upstairs, logic.mod.sve.has_grandpa_shed_repaired()) set_entrance_rule(multiworld, player, SVEEntrance.use_bear_shop, (logic.mod.sve.can_buy_bear_recipe())) set_entrance_rule(multiworld, player, SVEEntrance.railroad_to_grampleton_station, logic.received(SVEQuestItem.scarlett_job_offer)) set_entrance_rule(multiworld, player, SVEEntrance.museum_to_gunther_bedroom, logic.relationship.has_hearts(ModNPC.gunther, 2)) @@ -891,10 +985,11 @@ def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, worl def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - set_entrance_rule(multiworld, player, SVEEntrance.summit_to_highlands, logic.received(SVEQuestItem.marlon_boat_paddle)) + set_entrance_rule(multiworld, player, SVEEntrance.summit_to_highlands, logic.mod.sve.has_marlon_boat()) set_entrance_rule(multiworld, player, SVEEntrance.wizard_to_fable_reef, logic.received(SVEQuestItem.fable_reef_portal)) set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_cave, logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) & logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) + set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_pond, logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): @@ -904,12 +999,17 @@ def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player def set_entrance_rule(multiworld, player, entrance: str, rule: StardewRule): - potentially_required_regions = look_for_indirect_connection(rule) - if potentially_required_regions: - for region in potentially_required_regions: - multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player)) - - MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule) + try: + potentially_required_regions = look_for_indirect_connection(rule) + if potentially_required_regions: + for region in potentially_required_regions: + logger.debug(f"Registering indirect condition for {region} -> {entrance}") + multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player)) + + MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule) + except KeyError as ex: + logger.error(f"""Failed to evaluate indirect connection in: {explain(rule, CollectionState(multiworld))}""") + raise ex def set_island_entrance_rule(multiworld, player, entrance: str, rule: StardewRule, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/scripts/export_locations.py b/worlds/stardew_valley/scripts/export_locations.py index 1dc60f79b14b..c181faec7b94 100644 --- a/worlds/stardew_valley/scripts/export_locations.py +++ b/worlds/stardew_valley/scripts/export_locations.py @@ -16,11 +16,17 @@ if __name__ == "__main__": with open("output/stardew_valley_location_table.json", "w+") as f: locations = { + "Cheat Console": + {"code": -1, "region": "Archipelago"}, + "Server": + {"code": -2, "region": "Archipelago"} + } + locations.update({ location.name: { "code": location.code, "region": location.region, } for location in location_table.values() if location.code is not None - } + }) json.dump({"locations": locations}, f) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 007d2b64dc41..af4c3c35330d 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -1,7 +1,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections import deque +from collections import deque, Counter +from dataclasses import dataclass, field from functools import cached_property from itertools import chain from threading import Lock @@ -292,10 +293,13 @@ def __repr__(self): def __eq__(self, other): return (isinstance(other, type(self)) and self.combinable_rules == other.combinable_rules and - self.simplification_state.original_simplifiable_rules == self.simplification_state.original_simplifiable_rules) + self.simplification_state.original_simplifiable_rules == other.simplification_state.original_simplifiable_rules) def __hash__(self): - return hash((id(self.combinable_rules), self.simplification_state.original_simplifiable_rules)) + if len(self.combinable_rules) + len(self.simplification_state.original_simplifiable_rules) > 5: + return id(self) + + return hash((*self.combinable_rules.values(), self.simplification_state.original_simplifiable_rules)) class Or(AggregatingStardewRule): @@ -323,9 +327,6 @@ def __or__(self, other): def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule: return min(left, right, key=lambda x: x.value) - def get_difficulty(self): - return min(rule.get_difficulty() for rule in self.original_rules) - class And(AggregatingStardewRule): identity = true_ @@ -352,19 +353,34 @@ def __and__(self, other): def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule: return max(left, right, key=lambda x: x.value) - def get_difficulty(self): - return max(rule.get_difficulty() for rule in self.original_rules) - class Count(BaseStardewRule): count: int rules: List[StardewRule] + counter: Counter[StardewRule] + evaluate: Callable[[CollectionState], bool] + + total: Optional[int] + rule_mapping: Optional[Dict[StardewRule, StardewRule]] def __init__(self, rules: List[StardewRule], count: int): - self.rules = rules self.count = count + self.counter = Counter(rules) + + if len(self.counter) / len(rules) < .66: + # Checking if it's worth using the count operation with shortcircuit or not. Value should be fine-tuned when Count has more usage. + self.total = sum(self.counter.values()) + self.rules = sorted(self.counter.keys(), key=lambda x: self.counter[x], reverse=True) + self.rule_mapping = {} + self.evaluate = self.evaluate_with_shortcircuit + else: + self.rules = rules + self.evaluate = self.evaluate_without_shortcircuit - def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: + def __call__(self, state: CollectionState) -> bool: + return self.evaluate(state) + + def evaluate_without_shortcircuit(self, state: CollectionState) -> bool: c = 0 for i in range(self.rules_count): self.rules[i], value = self.rules[i].evaluate_while_simplifying(state) @@ -372,37 +388,58 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul c += 1 if c >= self.count: - return self, True + return True if c + self.rules_count - i < self.count: break - return self, False + return False - def __call__(self, state: CollectionState) -> bool: - return self.evaluate_while_simplifying(state)[1] + def evaluate_with_shortcircuit(self, state: CollectionState) -> bool: + c = 0 + t = self.total + + for rule in self.rules: + evaluation_value = self.call_evaluate_while_simplifying_cached(rule, state) + rule_value = self.counter[rule] + + if evaluation_value: + c += rule_value + else: + t -= rule_value + + if c >= self.count: + return True + elif t < self.count: + break + + return False + + def call_evaluate_while_simplifying_cached(self, rule: StardewRule, state: CollectionState) -> bool: + try: + # A mapping table with the original rule is used here because two rules could resolve to the same rule. + # This would require to change the counter to merge both rules, and quickly become complicated. + return self.rule_mapping[rule](state) + except KeyError: + self.rule_mapping[rule], value = rule.evaluate_while_simplifying(state) + return value + + def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: + return self, self(state) @cached_property def rules_count(self): return len(self.rules) - def get_difficulty(self): - self.rules = sorted(self.rules, key=lambda x: x.get_difficulty()) - # In an optimal situation, all the simplest rules will be true. Since the rules are sorted, we know that the most difficult rule we might have to do - # is the one at the "self.count". - return self.rules[self.count - 1].get_difficulty() - def __repr__(self): - return f"Received {self.count} {repr(self.rules)}" + return f"Received {self.count} [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]" +@dataclass(frozen=True) class Has(BaseStardewRule): item: str # For sure there is a better way than just passing all the rules everytime - other_rules: Dict[str, StardewRule] - - def __init__(self, item: str, other_rules: Dict[str, StardewRule]): - self.item = item - self.other_rules = other_rules + other_rules: Dict[str, StardewRule] = field(repr=False, hash=False, compare=False) + group: str = "item" def __call__(self, state: CollectionState) -> bool: return self.evaluate_while_simplifying(state)[1] @@ -410,21 +447,15 @@ def __call__(self, state: CollectionState) -> bool: def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self.other_rules[self.item].evaluate_while_simplifying(state) - def get_difficulty(self): - return self.other_rules[self.item].get_difficulty() + 1 - def __str__(self): if self.item not in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item}" + return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}" + return f"Has {self.item} ({self.group})" def __repr__(self): if self.item not in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item} -> {repr(self.other_rules[self.item])}" - - def __hash__(self): - return hash(self.item) + return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}" + return f"Has {self.item} ({self.group}) -> {repr(self.other_rules[self.item])}" class RepeatableChain(Iterable, Sized): diff --git a/worlds/stardew_valley/stardew_rule/indirect_connection.py b/worlds/stardew_valley/stardew_rule/indirect_connection.py index 2bbddb16818f..17433f7df4a8 100644 --- a/worlds/stardew_valley/stardew_rule/indirect_connection.py +++ b/worlds/stardew_valley/stardew_rule/indirect_connection.py @@ -6,34 +6,38 @@ def look_for_indirect_connection(rule: StardewRule) -> Set[str]: required_regions = set() - _find(rule, required_regions) + _find(rule, required_regions, depth=0) return required_regions @singledispatch -def _find(rule: StardewRule, regions: Set[str]): +def _find(rule: StardewRule, regions: Set[str], depth: int): ... @_find.register -def _(rule: AggregatingStardewRule, regions: Set[str]): +def _(rule: AggregatingStardewRule, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" for r in rule.original_rules: - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Count, regions: Set[str]): +def _(rule: Count, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" for r in rule.rules: - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Has, regions: Set[str]): +def _(rule: Has, regions: Set[str], depth: int): + assert depth < 50, f"Recursion depth exceeded on {rule.item}" r = rule.other_rules[rule.item] - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Reach, regions: Set[str]): +def _(rule: Reach, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" if rule.resolution_hint == "Region": regions.add(rule.spot) diff --git a/worlds/stardew_valley/stardew_rule/literal.py b/worlds/stardew_valley/stardew_rule/literal.py index 58f7bae047fa..93a8503e8739 100644 --- a/worlds/stardew_valley/stardew_rule/literal.py +++ b/worlds/stardew_valley/stardew_rule/literal.py @@ -33,9 +33,6 @@ def __or__(self, other) -> StardewRule: def __and__(self, other) -> StardewRule: return other - def get_difficulty(self): - return 0 - class False_(LiteralStardewRule): # noqa value = False @@ -52,9 +49,6 @@ def __or__(self, other) -> StardewRule: def __and__(self, other) -> StardewRule: return self - def get_difficulty(self): - return 999999999 - false_ = False_() true_ = True_() diff --git a/worlds/stardew_valley/stardew_rule/protocol.py b/worlds/stardew_valley/stardew_rule/protocol.py index c20394d5b826..f69a3663c63a 100644 --- a/worlds/stardew_valley/stardew_rule/protocol.py +++ b/worlds/stardew_valley/stardew_rule/protocol.py @@ -24,7 +24,3 @@ def __or__(self, other: StardewRule): @abstractmethod def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: ... - - @abstractmethod - def get_difficulty(self): - ... diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py new file mode 100644 index 000000000000..2e2b9c959d7f --- /dev/null +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import cached_property, singledispatch +from typing import Iterable, Set, Tuple, List, Optional + +from BaseClasses import CollectionState, Location, Entrance +from worlds.generic.Rules import CollectionRule +from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach, true_ + + +@dataclass +class RuleExplanation: + rule: StardewRule + state: CollectionState = field(repr=False, hash=False) + expected: bool + sub_rules: Iterable[StardewRule] = field(default_factory=list) + explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set, repr=False, hash=False) + current_rule_explored: bool = False + + def __post_init__(self): + checkpoint = _rule_key(self.rule) + if checkpoint is not None and checkpoint in self.explored_rules_key: + self.current_rule_explored = True + self.sub_rules = [] + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + def __str__(self, depth=0): + if not self.sub_rules: + return self.summary(depth) + + return self.summary(depth) + "\n" + "\n".join(i.__str__(depth + 1) + if i.result is not self.expected else i.summary(depth + 1) + for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) + + @cached_property + def result(self) -> bool: + try: + return self.rule(self.state) + except KeyError: + return False + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + rule_key = _rule_key(self.rule) + if rule_key is not None: + self.explored_rules_key.add(rule_key) + + return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules] + + +@dataclass +class CountSubRuleExplanation(RuleExplanation): + count: int = 1 + + @staticmethod + def from_explanation(expl: RuleExplanation, count: int) -> CountSubRuleExplanation: + return CountSubRuleExplanation(expl.rule, expl.state, expl.expected, expl.sub_rules, expl.explored_rules_key, expl.current_rule_explored, count) + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{self.count}x {str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + +@dataclass +class CountExplanation(RuleExplanation): + rule: Count + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + return [ + CountSubRuleExplanation.from_explanation(_explain(rule, self.state, self.expected, self.explored_rules_key), count) + for rule, count in self.rule.counter.items() + ] + + +def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: + if isinstance(rule, StardewRule): + return _explain(rule, state, expected, explored_spots=set()) + else: + return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa + + +@singledispatch +def _explain(rule: StardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, rule.original_rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return CountExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Has, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + try: + return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]], explored_rules_key=explored_spots) + except KeyError: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: TotalReceived, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items], explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + access_rules = None + if rule.resolution_hint == 'Location': + spot = state.multiworld.get_location(rule.spot, rule.player) + + if isinstance(spot.access_rule, StardewRule): + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + elif spot.access_rule == Location.access_rule: + # Sometime locations just don't have an access rule and all the relevant logic is in the parent region. + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + + + elif rule.resolution_hint == 'Entrance': + spot = state.multiworld.get_entrance(rule.spot, rule.player) + + if isinstance(spot.access_rule, StardewRule): + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + elif spot.access_rule == Entrance.access_rule: + # Sometime entrances just don't have an access rule and all the relevant logic is in the parent region. + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + + else: + spot = state.multiworld.get_region(rule.spot, rule.player) + access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)] + + if not access_rules: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Received, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + access_rules = None + if rule.event: + try: + spot = state.multiworld.get_location(rule.item, rule.player) + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + except KeyError: + pass + + if not access_rules: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots) + + +@singledispatch +def _rule_key(_: StardewRule) -> Optional[Tuple[str, str]]: + return None + + +@_rule_key.register +def _(rule: Reach) -> Tuple[str, str]: + return rule.spot, rule.resolution_hint + + +@_rule_key.register +def _(rule: Received) -> Optional[Tuple[str, str]]: + if not rule.event: + return None + + return rule.item, "Logic Event" diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index a0fce7c7c19e..6fc349a6274d 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -1,10 +1,12 @@ from dataclasses import dataclass -from typing import Iterable, Union, List, Tuple, Hashable +from typing import Iterable, Union, List, Tuple, Hashable, TYPE_CHECKING -from BaseClasses import ItemClassification, CollectionState +from BaseClasses import CollectionState from .base import BaseStardewRule, CombinableStardewRule from .protocol import StardewRule -from ..items import item_table + +if TYPE_CHECKING: + from .. import StardewValleyWorld class TotalReceived(BaseStardewRule): @@ -20,11 +22,6 @@ def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): else: items_list = [items] - assert items_list, "Can't create a Total Received conditions without items" - for item in items_list: - assert item_table[item].classification & ItemClassification.progression, \ - f"Item [{item_table[item].name}] has to be progression to be used in logic" - self.player = player self.items = items_list self.count = count @@ -40,9 +37,6 @@ def __call__(self, state: CollectionState) -> bool: def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self, self(state) - def get_difficulty(self): - return self.count - def __repr__(self): return f"Received {self.count} {self.items}" @@ -52,10 +46,8 @@ class Received(CombinableStardewRule): item: str player: int count: int - - def __post_init__(self): - assert item_table[self.item].classification & ItemClassification.progression, \ - f"Item [{item_table[self.item].name}] has to be progression to be used in logic" + event: bool = False + """Helps `explain` to know it can dig into a location with the same name.""" @property def combination_key(self) -> Hashable: @@ -73,11 +65,8 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul def __repr__(self): if self.count == 1: - return f"Received {self.item}" - return f"Received {self.count} {self.item}" - - def get_difficulty(self): - return self.count + return f"Received {'event ' if self.event else ''}{self.item}" + return f"Received {'event ' if self.event else ''}{self.count} {self.item}" @dataclass(frozen=True) @@ -97,9 +86,6 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul def __repr__(self): return f"Reach {self.resolution_hint} {self.spot}" - def get_difficulty(self): - return 1 - @dataclass(frozen=True) class HasProgressionPercent(CombinableStardewRule): @@ -119,22 +105,27 @@ def value(self): return self.percent def __call__(self, state: CollectionState) -> bool: - stardew_world = state.multiworld.worlds[self.player] + stardew_world: "StardewValleyWorld" = state.multiworld.worlds[self.player] total_count = stardew_world.total_progression_items needed_count = (total_count * self.percent) // 100 + player_state = state.prog_items[self.player] + + if needed_count <= len(player_state) - len(stardew_world.excluded_from_total_progression_items): + return True + total_count = 0 - for item in state.prog_items[self.player]: - item_count = state.prog_items[self.player][item] + for item, item_count in player_state.items(): + if item in stardew_world.excluded_from_total_progression_items: + continue + total_count += item_count if total_count >= needed_count: return True + return False def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self, self(state) def __repr__(self): - return f"HasProgressionPercent {self.percent}" - - def get_difficulty(self): - return self.percent + return f"Received {self.percent}% progression items" diff --git a/worlds/stardew_valley/strings/ap_names/ap_option_names.py b/worlds/stardew_valley/strings/ap_names/ap_option_names.py new file mode 100644 index 000000000000..7ff2cc783d11 --- /dev/null +++ b/worlds/stardew_valley/strings/ap_names/ap_option_names.py @@ -0,0 +1,19 @@ +class WalnutsanityOptionName: + puzzles = "Puzzles" + bushes = "Bushes" + dig_spots = "Dig Spots" + repeatables = "Repeatables" + + +class BuffOptionName: + luck = "Luck" + damage = "Damage" + defense = "Defense" + immunity = "Immunity" + health = "Health" + energy = "Energy" + bite = "Bite Rate" + fish_trap = "Fish Trap" + fishing_bar = "Fishing Bar Size" + quality = "Quality" + glow = "Glow" diff --git a/worlds/stardew_valley/strings/ap_names/buff_names.py b/worlds/stardew_valley/strings/ap_names/buff_names.py index 4ddd6fb5034f..0f311869aa9a 100644 --- a/worlds/stardew_valley/strings/ap_names/buff_names.py +++ b/worlds/stardew_valley/strings/ap_names/buff_names.py @@ -1,3 +1,13 @@ class Buff: movement = "Movement Speed Bonus" - luck = "Luck Bonus" \ No newline at end of file + luck = "Luck Bonus" + damage = "Damage Bonus" + defense = "Defense Bonus" + immunity = "Immunity Bonus" + health = "Health Bonus" + energy = "Energy Bonus" + bite_rate = "Bite Rate Bonus" + fish_trap = "Fish Trap Bonus" + fishing_bar = "Fishing Bar Size Bonus" + quality = "Quality Bonus" + glow = "Glow Bonus" diff --git a/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py index 68dad8e75287..6826b9234a30 100644 --- a/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py +++ b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py @@ -1,4 +1,6 @@ class CommunityUpgrade: + raccoon = "Progressive Raccoon" fruit_bats = "Fruit Bats" mushroom_boxes = "Mushroom Boxes" movie_theater = "Progressive Movie Theater" + mr_qi_plane_ride = "Mr Qi's Plane Ride" diff --git a/worlds/stardew_valley/strings/ap_names/event_names.py b/worlds/stardew_valley/strings/ap_names/event_names.py index 08b9d8f8131c..449bb6720964 100644 --- a/worlds/stardew_valley/strings/ap_names/event_names.py +++ b/worlds/stardew_valley/strings/ap_names/event_names.py @@ -1,6 +1,16 @@ +all_events = set() + + +def event(name: str): + all_events.add(name) + return name + + class Event: - victory = "Victory" - can_construct_buildings = "Can Construct Buildings" - start_dark_talisman_quest = "Start Dark Talisman Quest" - can_ship_items = "Can Ship Items" - can_shop_at_pierre = "Can Shop At Pierre's" + victory = event("Victory") + spring_farming = event("Spring Farming") + summer_farming = event("Summer Farming") + fall_farming = event("Fall Farming") + winter_farming = event("Winter Farming") + + received_walnuts = event("Received Walnuts") diff --git a/worlds/stardew_valley/strings/ap_names/mods/__init__.py b/worlds/stardew_valley/strings/ap_names/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py index ccc2765544a6..58371aebe7ed 100644 --- a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py +++ b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py @@ -9,6 +9,10 @@ class DeepWoodsItem: class SkillLevel: + cooking = "Cooking Level" + binning = "Binning Level" + magic = "Magic Level" + socializing = "Socializing Level" luck = "Luck Level" archaeology = "Archaeology Level" @@ -25,8 +29,10 @@ class SVEQuestItem: fable_reef_portal = "Fable Reef Portal" grandpa_shed = "Grandpa's Shed" - sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, kittyfish_spell, scarlett_job_offer, morgan_schooling, grandpa_shed] - sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle, fable_reef_portal] + sve_always_quest_items: List[str] = [kittyfish_spell, scarlett_job_offer, morgan_schooling] + sve_always_quest_items_ginger_island: List[str] = [fable_reef_portal] + sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, grandpa_shed] + sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle] class SVELocation: diff --git a/worlds/stardew_valley/strings/artisan_good_names.py b/worlds/stardew_valley/strings/artisan_good_names.py index a017cff1f9dd..366189568cf7 100644 --- a/worlds/stardew_valley/strings/artisan_good_names.py +++ b/worlds/stardew_valley/strings/artisan_good_names.py @@ -21,6 +21,45 @@ class ArtisanGood: caviar = "Caviar" green_tea = "Green Tea" mead = "Mead" + mystic_syrup = "Mystic Syrup" + dried_fruit = "Dried Fruit" + dried_mushroom = "Dried Mushrooms" + raisins = "Raisins" + stardrop_tea = "Stardrop Tea" + smoked_fish = "Smoked Fish" + targeted_bait = "Targeted Bait" + + @classmethod + def specific_wine(cls, fruit: str) -> str: + return f"{cls.wine} [{fruit}]" + + @classmethod + def specific_juice(cls, vegetable: str) -> str: + return f"{cls.juice} [{vegetable}]" + + @classmethod + def specific_jelly(cls, fruit: str) -> str: + return f"{cls.jelly} [{fruit}]" + + @classmethod + def specific_pickles(cls, vegetable: str) -> str: + return f"{cls.pickles} [{vegetable}]" + + @classmethod + def specific_dried_fruit(cls, food: str) -> str: + return f"{cls.dried_fruit} [{food}]" + + @classmethod + def specific_dried_mushroom(cls, food: str) -> str: + return f"{cls.dried_mushroom} [{food}]" + + @classmethod + def specific_smoked_fish(cls, fish: str) -> str: + return f"{cls.smoked_fish} [{fish}]" + + @classmethod + def specific_bait(cls, fish: str) -> str: + return f"{cls.targeted_bait} [{fish}]" class ModArtisanGood: diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py new file mode 100644 index 000000000000..6c271f42ae9c --- /dev/null +++ b/worlds/stardew_valley/strings/book_names.py @@ -0,0 +1,61 @@ +class Book: + animal_catalogue = "Animal Catalogue" + book_of_mysteries = "Book of Mysteries" + book_of_stars = "Book Of Stars" + stardew_valley_almanac = "Stardew Valley Almanac" + bait_and_bobber = "Bait And Bobber" + mining_monthly = "Mining Monthly" + combat_quarterly = "Combat Quarterly" + woodcutters_weekly = "Woodcutter's Weekly" + the_alleyway_buffet = "The Alleyway Buffet" + the_art_o_crabbing = "The Art O' Crabbing" + dwarvish_safety_manual = "Dwarvish Safety Manual" + jewels_of_the_sea = "Jewels Of The Sea" + raccoon_journal = "Raccoon Journal" + woodys_secret = "Woody's Secret" + jack_be_nimble_jack_be_thick = "Jack Be Nimble, Jack Be Thick" + friendship_101 = "Friendship 101" + monster_compendium = "Monster Compendium" + mapping_cave_systems = "Mapping Cave Systems" + treasure_appraisal_guide = "Treasure Appraisal Guide" + way_of_the_wind_pt_1 = "Way Of The Wind pt. 1" + way_of_the_wind_pt_2 = "Way Of The Wind pt. 2" + horse_the_book = "Horse: The Book" + ol_slitherlegs = "Ol' Slitherlegs" + queen_of_sauce_cookbook = "Queen Of Sauce Cookbook" + price_catalogue = "Price Catalogue" + the_diamond_hunter = "The Diamond Hunter" + + +ordered_lost_books = [] +all_lost_books = set() + + +def lost_book(book_name: str): + ordered_lost_books.append(book_name) + all_lost_books.add(book_name) + return book_name + + +class LostBook: + tips_on_farming = lost_book("Tips on Farming") + this_is_a_book_by_marnie = lost_book("This is a book by Marnie") + on_foraging = lost_book("On Foraging") + the_fisherman_act_1 = lost_book("The Fisherman, Act 1") + how_deep_do_the_mines_go = lost_book("How Deep do the mines go?") + an_old_farmers_journal = lost_book("An Old Farmer's Journal") + scarecrows = lost_book("Scarecrows") + the_secret_of_the_stardrop = lost_book("The Secret of the Stardrop") + journey_of_the_prairie_king_the_smash_hit_video_game = lost_book("Journey of the Prairie King -- The Smash Hit Video Game!") + a_study_on_diamond_yields = lost_book("A Study on Diamond Yields") + brewmasters_guide = lost_book("Brewmaster's Guide") + mysteries_of_the_dwarves = lost_book("Mysteries of the Dwarves") + highlights_from_the_book_of_yoba = lost_book("Highlights From The Book of Yoba") + marriage_guide_for_farmers = lost_book("Marriage Guide for Farmers") + the_fisherman_act_ii = lost_book("The Fisherman, Act II") + technology_report = lost_book("Technology Report!") + secrets_of_the_legendary_fish = lost_book("Secrets of the Legendary Fish") + gunther_tunnel_notice = lost_book("Gunther Tunnel Notice") + note_from_gunther = lost_book("Note From Gunther") + goblins_by_m_jasper = lost_book("Goblins by M. Jasper") + secret_statues_acrostics = lost_book("Secret Statues Acrostics") diff --git a/worlds/stardew_valley/strings/bundle_names.py b/worlds/stardew_valley/strings/bundle_names.py index de8d8af3877f..5f560a545434 100644 --- a/worlds/stardew_valley/strings/bundle_names.py +++ b/worlds/stardew_valley/strings/bundle_names.py @@ -6,75 +6,103 @@ class CCRoom: vault = "Vault" boiler_room = "Boiler Room" abandoned_joja_mart = "Abandoned Joja Mart" + raccoon_requests = "Raccoon Requests" + + +all_cc_bundle_names = [] + + +def cc_bundle(name: str) -> str: + all_cc_bundle_names.append(name) + return name class BundleName: - spring_foraging = "Spring Foraging Bundle" - summer_foraging = "Summer Foraging Bundle" - fall_foraging = "Fall Foraging Bundle" - winter_foraging = "Winter Foraging Bundle" - construction = "Construction Bundle" - exotic_foraging = "Exotic Foraging Bundle" - beach_foraging = "Beach Foraging Bundle" - mines_foraging = "Mines Foraging Bundle" - desert_foraging = "Desert Foraging Bundle" - island_foraging = "Island Foraging Bundle" - sticky = "Sticky Bundle" - wild_medicine = "Wild Medicine Bundle" - quality_foraging = "Quality Foraging Bundle" - spring_crops = "Spring Crops Bundle" - summer_crops = "Summer Crops Bundle" - fall_crops = "Fall Crops Bundle" - quality_crops = "Quality Crops Bundle" - animal = "Animal Bundle" - artisan = "Artisan Bundle" - rare_crops = "Rare Crops Bundle" - fish_farmer = "Fish Farmer's Bundle" - garden = "Garden Bundle" - brewer = "Brewer's Bundle" - orchard = "Orchard Bundle" - island_crops = "Island Crops Bundle" - agronomist = "Agronomist's Bundle" - slime_farmer = "Slime Farmer Bundle" - river_fish = "River Fish Bundle" - lake_fish = "Lake Fish Bundle" - ocean_fish = "Ocean Fish Bundle" - night_fish = "Night Fishing Bundle" - crab_pot = "Crab Pot Bundle" - trash = "Trash Bundle" - recycling = "Recycling Bundle" - specialty_fish = "Specialty Fish Bundle" - spring_fish = "Spring Fishing Bundle" - summer_fish = "Summer Fishing Bundle" - fall_fish = "Fall Fishing Bundle" - winter_fish = "Winter Fishing Bundle" - rain_fish = "Rain Fishing Bundle" - quality_fish = "Quality Fish Bundle" - master_fisher = "Master Fisher's Bundle" - legendary_fish = "Legendary Fish Bundle" - island_fish = "Island Fish Bundle" - deep_fishing = "Deep Fishing Bundle" - tackle = "Tackle Bundle" - bait = "Master Baiter Bundle" - blacksmith = "Blacksmith's Bundle" - geologist = "Geologist's Bundle" - adventurer = "Adventurer's Bundle" - treasure_hunter = "Treasure Hunter's Bundle" - engineer = "Engineer's Bundle" - demolition = "Demolition Bundle" - paleontologist = "Paleontologist's Bundle" - archaeologist = "Archaeologist's Bundle" - chef = "Chef's Bundle" - dye = "Dye Bundle" - field_research = "Field Research Bundle" - fodder = "Fodder Bundle" - enchanter = "Enchanter's Bundle" - children = "Children's Bundle" - forager = "Forager's Bundle" - home_cook = "Home Cook's Bundle" - bartender = "Bartender's Bundle" - gambler = "Gambler's Bundle" - carnival = "Carnival Bundle" - walnut_hunter = "Walnut Hunter Bundle" - qi_helper = "Qi's Helper Bundle" + spring_foraging = cc_bundle("Spring Foraging Bundle") + summer_foraging = cc_bundle("Summer Foraging Bundle") + fall_foraging = cc_bundle("Fall Foraging Bundle") + winter_foraging = cc_bundle("Winter Foraging Bundle") + construction = cc_bundle("Construction Bundle") + exotic_foraging = cc_bundle("Exotic Foraging Bundle") + beach_foraging = cc_bundle("Beach Foraging Bundle") + mines_foraging = cc_bundle("Mines Foraging Bundle") + desert_foraging = cc_bundle("Desert Foraging Bundle") + island_foraging = cc_bundle("Island Foraging Bundle") + sticky = cc_bundle("Sticky Bundle") + forest = cc_bundle("Forest Bundle") + green_rain = cc_bundle("Green Rain Bundle") + wild_medicine = cc_bundle("Wild Medicine Bundle") + quality_foraging = cc_bundle("Quality Foraging Bundle") + spring_crops = cc_bundle("Spring Crops Bundle") + summer_crops = cc_bundle("Summer Crops Bundle") + fall_crops = cc_bundle("Fall Crops Bundle") + quality_crops = cc_bundle("Quality Crops Bundle") + animal = cc_bundle("Animal Bundle") + artisan = cc_bundle("Artisan Bundle") + rare_crops = cc_bundle("Rare Crops Bundle") + fish_farmer = cc_bundle("Fish Farmer's Bundle") + garden = cc_bundle("Garden Bundle") + brewer = cc_bundle("Brewer's Bundle") + orchard = cc_bundle("Orchard Bundle") + island_crops = cc_bundle("Island Crops Bundle") + agronomist = cc_bundle("Agronomist's Bundle") + slime_farmer = cc_bundle("Slime Farmer Bundle") + sommelier = cc_bundle("Sommelier Bundle") + dry = cc_bundle("Dry Bundle") + river_fish = cc_bundle("River Fish Bundle") + lake_fish = cc_bundle("Lake Fish Bundle") + ocean_fish = cc_bundle("Ocean Fish Bundle") + night_fish = cc_bundle("Night Fishing Bundle") + crab_pot = cc_bundle("Crab Pot Bundle") + trash = cc_bundle("Trash Bundle") + recycling = cc_bundle("Recycling Bundle") + specialty_fish = cc_bundle("Specialty Fish Bundle") + spring_fish = cc_bundle("Spring Fishing Bundle") + summer_fish = cc_bundle("Summer Fishing Bundle") + fall_fish = cc_bundle("Fall Fishing Bundle") + winter_fish = cc_bundle("Winter Fishing Bundle") + rain_fish = cc_bundle("Rain Fishing Bundle") + quality_fish = cc_bundle("Quality Fish Bundle") + master_fisher = cc_bundle("Master Fisher's Bundle") + legendary_fish = cc_bundle("Legendary Fish Bundle") + island_fish = cc_bundle("Island Fish Bundle") + deep_fishing = cc_bundle("Deep Fishing Bundle") + tackle = cc_bundle("Tackle Bundle") + bait = cc_bundle("Master Baiter Bundle") + specific_bait = cc_bundle("Specific Fishing Bundle") + fish_smoker = cc_bundle("Fish Smoker Bundle") + blacksmith = cc_bundle("Blacksmith's Bundle") + geologist = cc_bundle("Geologist's Bundle") + adventurer = cc_bundle("Adventurer's Bundle") + treasure_hunter = cc_bundle("Treasure Hunter's Bundle") + engineer = cc_bundle("Engineer's Bundle") + demolition = cc_bundle("Demolition Bundle") + paleontologist = cc_bundle("Paleontologist's Bundle") + archaeologist = cc_bundle("Archaeologist's Bundle") + chef = cc_bundle("Chef's Bundle") + dye = cc_bundle("Dye Bundle") + field_research = cc_bundle("Field Research Bundle") + fodder = cc_bundle("Fodder Bundle") + enchanter = cc_bundle("Enchanter's Bundle") + children = cc_bundle("Children's Bundle") + forager = cc_bundle("Forager's Bundle") + home_cook = cc_bundle("Home Cook's Bundle") + helper = cc_bundle("Helper's Bundle") + spirit_eve = cc_bundle("Spirit's Eve Bundle") + winter_star = cc_bundle("Winter Star Bundle") + bartender = cc_bundle("Bartender's Bundle") + calico = cc_bundle("Calico Bundle") + raccoon = cc_bundle("Raccoon Bundle") + money_2500 = cc_bundle("2,500g Bundle") + money_5000 = cc_bundle("5,000g Bundle") + money_10000 = cc_bundle("10,000g Bundle") + money_25000 = cc_bundle("25,000g Bundle") + gambler = cc_bundle("Gambler's Bundle") + carnival = cc_bundle("Carnival Bundle") + walnut_hunter = cc_bundle("Walnut Hunter Bundle") + qi_helper = cc_bundle("Qi's Helper Bundle") missing_bundle = "The Missing Bundle" + raccoon_fish = "Raccoon Fish" + raccoon_artisan = "Raccoon Artisan" + raccoon_food = "Raccoon Food" + raccoon_foraging = "Raccoon Foraging" diff --git a/worlds/stardew_valley/strings/craftable_names.py b/worlds/stardew_valley/strings/craftable_names.py index 74a77a8e9467..83445c702c32 100644 --- a/worlds/stardew_valley/strings/craftable_names.py +++ b/worlds/stardew_valley/strings/craftable_names.py @@ -25,6 +25,7 @@ class WildSeeds: winter = "Winter Seeds" ancient = "Ancient Seeds" grass_starter = "Grass Starter" + blue_grass_starter = "Blue Grass Starter" tea_sapling = "Tea Sapling" fiber = "Fiber Seeds" @@ -48,6 +49,7 @@ class Floor: class Fishing: spinner = "Spinner" trap_bobber = "Trap Bobber" + sonar_bobber = "Sonar Bobber" cork_bobber = "Cork Bobber" quality_bobber = "Quality Bobber" treasure_hunter = "Treasure Hunter" @@ -59,6 +61,8 @@ class Fishing: magic_bait = "Magic Bait" lead_bobber = "Lead Bobber" curiosity_lure = "Curiosity Lure" + deluxe_bait = "Deluxe Bait" + challenge_bait = "Challenge Bait" class Ring: @@ -70,6 +74,7 @@ class Ring: glowstone_ring = "Glowstone Ring" iridium_band = "Iridium Band" wedding_ring = "Wedding Ring" + lucky_ring = "Lucky Ring" class Edible: @@ -88,6 +93,15 @@ class Consumable: warp_totem_desert = "Warp Totem: Desert" warp_totem_island = "Warp Totem: Island" rain_totem = "Rain Totem" + mystery_box = "Mystery Box" + gold_mystery_box = "Golden Mystery Box" + treasure_totem = "Treasure Totem" + fireworks_red = "Fireworks (Red)" + fireworks_purple = "Fireworks (Purple)" + fireworks_green = "Fireworks (Green)" + far_away_stone = "Far Away Stone" + golden_animal_cracker = "Golden Animal Cracker" + butterfly_powder = "Butterfly Powder" class Lighting: @@ -116,12 +130,20 @@ class Furniture: class Storage: chest = "Chest" stone_chest = "Stone Chest" + big_chest = "Big Chest" + big_stone_chest = "Big Stone Chest" class Sign: wood = "Wood Sign" stone = "Stone Sign" dark = "Dark Sign" + text = "Text Sign" + + +class Statue: + blessings = "Statue Of Blessings" + dwarf_king = "Statue Of The Dwarf King" class Craftable: @@ -137,6 +159,7 @@ class Craftable: farm_computer = "Farm Computer" hopper = "Hopper" cookout_kit = "Cookout Kit" + tent_kit = "Tent Kit" class ModEdible: @@ -152,9 +175,11 @@ class ModEdible: class ModCraftable: travel_core = "Travel Core" - glass_bazier = "Glass Bazier" + glass_brazier = "Glass Brazier" water_shifter = "Water Shifter" + rusty_brazier = "Rusty Brazier" glass_fence = "Glass Fence" + bone_fence = "Bone Fence" wooden_display = "Wooden Display" hardwood_display = "Hardwood Display" neanderthal_skeleton = "Neanderthal Skeleton" @@ -171,11 +196,17 @@ class ModMachine: hardwood_preservation_chamber = "Hardwood Preservation Chamber" grinder = "Grinder" ancient_battery = "Ancient Battery Production Station" + restoration_table = "Restoration Table" + trash_bin = "Trash Bin" + composter = "Composter" + recycling_bin = "Recycling Bin" + advanced_recycling_machine = "Advanced Recycling Machine" class ModFloor: glass_path = "Glass Path" bone_path = "Bone Path" + rusty_path = "Rusty Path" class ModConsumable: diff --git a/worlds/stardew_valley/strings/crop_names.py b/worlds/stardew_valley/strings/crop_names.py index 295e40005f75..fa7a77c834fc 100644 --- a/worlds/stardew_valley/strings/crop_names.py +++ b/worlds/stardew_valley/strings/crop_names.py @@ -1,64 +1,55 @@ -all_fruits = [] -all_vegetables = [] - - -def veggie(name: str) -> str: - all_vegetables.append(name) - return name - - -def fruity(name: str) -> str: - all_fruits.append(name) - return name - - class Fruit: - sweet_gem_berry = fruity("Sweet Gem Berry") + sweet_gem_berry = "Sweet Gem Berry" any = "Any Fruit" - blueberry = fruity("Blueberry") - melon = fruity("Melon") - apple = fruity("Apple") - apricot = fruity("Apricot") - cherry = fruity("Cherry") - orange = fruity("Orange") - peach = fruity("Peach") - pomegranate = fruity("Pomegranate") - banana = fruity("Banana") - mango = fruity("Mango") - pineapple = fruity("Pineapple") - ancient_fruit = fruity("Ancient Fruit") - strawberry = fruity("Strawberry") - starfruit = fruity("Starfruit") - rhubarb = fruity("Rhubarb") - grape = fruity("Grape") - cranberries = fruity("Cranberries") - hot_pepper = fruity("Hot Pepper") + blueberry = "Blueberry" + melon = "Melon" + apple = "Apple" + apricot = "Apricot" + cherry = "Cherry" + orange = "Orange" + peach = "Peach" + pomegranate = "Pomegranate" + banana = "Banana" + mango = "Mango" + pineapple = "Pineapple" + ancient_fruit = "Ancient Fruit" + strawberry = "Strawberry" + starfruit = "Starfruit" + rhubarb = "Rhubarb" + grape = "Grape" + cranberries = "Cranberries" + hot_pepper = "Hot Pepper" + powdermelon = "Powdermelon" + qi_fruit = "Qi Fruit" class Vegetable: any = "Any Vegetable" - parsnip = veggie("Parsnip") - garlic = veggie("Garlic") + parsnip = "Parsnip" + garlic = "Garlic" bok_choy = "Bok Choy" wheat = "Wheat" - potato = veggie("Potato") - corn = veggie("Corn") - tomato = veggie("Tomato") - pumpkin = veggie("Pumpkin") - unmilled_rice = veggie("Unmilled Rice") - beet = veggie("Beet") + potato = "Potato" + corn = "Corn" + tomato = "Tomato" + pumpkin = "Pumpkin" + unmilled_rice = "Unmilled Rice" + beet = "Beet" hops = "Hops" - cauliflower = veggie("Cauliflower") - amaranth = veggie("Amaranth") - kale = veggie("Kale") - artichoke = veggie("Artichoke") + cauliflower = "Cauliflower" + amaranth = "Amaranth" + kale = "Kale" + artichoke = "Artichoke" tea_leaves = "Tea Leaves" - eggplant = veggie("Eggplant") - green_bean = veggie("Green Bean") - red_cabbage = veggie("Red Cabbage") - yam = veggie("Yam") - radish = veggie("Radish") - taro_root = veggie("Taro Root") + eggplant = "Eggplant" + green_bean = "Green Bean" + red_cabbage = "Red Cabbage" + yam = "Yam" + radish = "Radish" + taro_root = "Taro Root" + carrot = "Carrot" + summer_squash = "Summer Squash" + broccoli = "Broccoli" class SVEFruit: @@ -76,7 +67,3 @@ class SVEVegetable: class DistantLandsCrop: void_mint = "Void Mint Leaves" vile_ancient_fruit = "Vile Ancient Fruit" - - -all_vegetables = tuple(all_vegetables) -all_fruits = tuple(all_fruits) diff --git a/worlds/stardew_valley/strings/currency_names.py b/worlds/stardew_valley/strings/currency_names.py index 5192466c9ca7..21ccb5b55c58 100644 --- a/worlds/stardew_valley/strings/currency_names.py +++ b/worlds/stardew_valley/strings/currency_names.py @@ -5,6 +5,9 @@ class Currency: star_token = "Star Token" money = "Money" cinder_shard = "Cinder Shard" + prize_ticket = "Prize Ticket" + calico_egg = "Calico Egg" + golden_tag = "Golden Tag" @staticmethod def is_currency(item: str) -> bool: diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 00823d62ea07..b1c84004eb7a 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -42,14 +42,7 @@ class Entrance: forest_to_marnie_ranch = "Forest to Marnie's Ranch" forest_to_leah_cottage = "Forest to Leah's Cottage" forest_to_sewer = "Forest to Sewer" - buy_from_traveling_merchant = "Buy from Traveling Merchant" - buy_from_traveling_merchant_sunday = "Buy from Traveling Merchant Sunday" - buy_from_traveling_merchant_monday = "Buy from Traveling Merchant Monday" - buy_from_traveling_merchant_tuesday = "Buy from Traveling Merchant Tuesday" - buy_from_traveling_merchant_wednesday = "Buy from Traveling Merchant Wednesday" - buy_from_traveling_merchant_thursday = "Buy from Traveling Merchant Thursday" - buy_from_traveling_merchant_friday = "Buy from Traveling Merchant Friday" - buy_from_traveling_merchant_saturday = "Buy from Traveling Merchant Saturday" + forest_to_mastery_cave = "Forest to Mastery Cave" mountain_to_railroad = "Mountain to Railroad" mountain_to_tent = "Mountain to Tent" mountain_to_carpenter_shop = "Mountain to Carpenter Shop" @@ -57,6 +50,7 @@ class Entrance: mountain_to_the_mines = "Mountain to The Mines" enter_quarry = "Mountain to Quarry" mountain_to_adventurer_guild = "Mountain to Adventurer's Guild" + adventurer_guild_to_bedroom = "Adventurer's Guild to Marlon's Bedroom" mountain_to_town = "Mountain to Town" town_to_community_center = "Town to Community Center" access_crafts_room = "Access Crafts Room" @@ -100,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" @@ -120,7 +115,6 @@ class Entrance: mine_to_skull_cavern_floor_175 = dig_to_skull_floor(175) mine_to_skull_cavern_floor_200 = dig_to_skull_floor(200) enter_dangerous_skull_cavern = "Enter the Dangerous Skull Cavern" - talk_to_mines_dwarf = "Talk to Mines Dwarf" dig_to_mines_floor_5 = dig_to_mines_floor(5) dig_to_mines_floor_10 = dig_to_mines_floor(10) dig_to_mines_floor_15 = dig_to_mines_floor(15) @@ -183,6 +177,19 @@ class Entrance: parrot_express_jungle_to_docks = "Parrot Express Jungle to Docks" parrot_express_dig_site_to_docks = "Parrot Express Dig Site to Docks" parrot_express_volcano_to_docks = "Parrot Express Volcano to Docks" + + +class LogicEntrance: + talk_to_mines_dwarf = "Talk to Mines Dwarf" + + buy_from_traveling_merchant = "Buy from Traveling Merchant" + buy_from_traveling_merchant_sunday = "Buy from Traveling Merchant Sunday" + buy_from_traveling_merchant_monday = "Buy from Traveling Merchant Monday" + buy_from_traveling_merchant_tuesday = "Buy from Traveling Merchant Tuesday" + buy_from_traveling_merchant_wednesday = "Buy from Traveling Merchant Wednesday" + buy_from_traveling_merchant_thursday = "Buy from Traveling Merchant Thursday" + buy_from_traveling_merchant_friday = "Buy from Traveling Merchant Friday" + buy_from_traveling_merchant_saturday = "Buy from Traveling Merchant Saturday" farmhouse_cooking = "Farmhouse Cooking" island_cooking = "Island Cooking" shipping = "Use Shipping Bin" @@ -191,17 +198,43 @@ class Entrance: blacksmith_iron = "Upgrade Iron Tools" blacksmith_gold = "Upgrade Gold Tools" blacksmith_iridium = "Upgrade Iridium Tools" - farming = "Start Farming" + + grow_spring_crops = "Grow Spring Crops" + grow_summer_crops = "Grow Summer Crops" + grow_fall_crops = "Grow Fall Crops" + grow_winter_crops = "Grow Winter Crops" + grow_spring_crops_in_greenhouse = "Grow Spring Crops in Greenhouse" + grow_summer_crops_in_greenhouse = "Grow Summer Crops in Greenhouse" + grow_fall_crops_in_greenhouse = "Grow Fall Crops in Greenhouse" + grow_winter_crops_in_greenhouse = "Grow Winter Crops in Greenhouse" + grow_indoor_crops_in_greenhouse = "Grow Indoor Crops in Greenhouse" + grow_spring_crops_on_island = "Grow Spring Crops on Island" + grow_summer_crops_on_island = "Grow Summer Crops on Island" + grow_fall_crops_on_island = "Grow Fall Crops on Island" + grow_winter_crops_on_island = "Grow Winter Crops on Island" + grow_indoor_crops_on_island = "Grow Indoor Crops on Island" + grow_summer_fall_crops_in_summer = "Grow Summer Fall Crops in Summer" + grow_summer_fall_crops_in_fall = "Grow Summer Fall Crops in Fall" + fishing = "Start Fishing" attend_egg_festival = "Attend Egg Festival" + attend_desert_festival = "Attend Desert Festival" attend_flower_dance = "Attend Flower Dance" attend_luau = "Attend Luau" + attend_trout_derby = "Attend Trout Derby" attend_moonlight_jellies = "Attend Dance of the Moonlight Jellies" attend_fair = "Attend Stardew Valley Fair" attend_spirit_eve = "Attend Spirit's Eve" attend_festival_of_ice = "Attend Festival of Ice" attend_night_market = "Attend Night Market" attend_winter_star = "Attend Feast of the Winter Star" + attend_squidfest = "Attend SquidFest" + buy_experience_books = "Buy Experience Books from the bookseller" + buy_year1_books = "Buy Year 1 Books from the Bookseller" + buy_year3_books = "Buy Year 3 Books from the Bookseller" + complete_raccoon_requests = "Complete Raccoon Requests" + buy_from_raccoon = "Buy From Raccoon" + fish_in_waterfall = "Fish In Waterfall" # Skull Cavern Elevator @@ -326,6 +359,7 @@ class SVEEntrance: sprite_spring_to_cave = "Sprite Spring to Sprite Spring Cave" fish_shop_to_willy_bedroom = "Willy's Fish Shop to Willy's Bedroom" museum_to_gunther_bedroom = "Museum to Gunther's Bedroom" + highlands_to_pond = "Highlands to Highlands Pond" class AlectoEntrance: @@ -356,4 +390,3 @@ class BoardingHouseEntrance: lost_valley_ruins_to_lost_valley_house_1 = "Lost Valley Ruins to Lost Valley Ruins - First House" lost_valley_ruins_to_lost_valley_house_2 = "Lost Valley Ruins to Lost Valley Ruins - Second House" boarding_house_plateau_to_buffalo_ranch = "Boarding House Outside to Buffalo's Ranch" - diff --git a/worlds/stardew_valley/strings/festival_check_names.py b/worlds/stardew_valley/strings/festival_check_names.py index 73a9d3978eab..b59b3cd03f17 100644 --- a/worlds/stardew_valley/strings/festival_check_names.py +++ b/worlds/stardew_valley/strings/festival_check_names.py @@ -35,3 +35,46 @@ class FestivalCheck: jack_o_lantern = "Jack-O-Lantern Recipe" moonlight_jellies_banner = "Moonlight Jellies Banner" starport_decal = "Starport Decal" + calico_race = "Calico Race" + mummy_mask = "Mummy Mask" + calico_statue = "Calico Statue" + emily_outfit_service = "Emily's Outfit Services" + earthy_mousse = "Earthy Mousse" + sweet_bean_cake = "Sweet Bean Cake" + skull_cave_casserole = "Skull Cave Casserole" + spicy_tacos = "Spicy Tacos" + mountain_chili = "Mountain Chili" + crystal_cake = "Crystal Cake" + cave_kebab = "Cave Kebab" + hot_log = "Hot Log" + sour_salad = "Sour Salad" + superfood_cake = "Superfood Cake" + warrior_smoothie = "Warrior Smoothie" + rumpled_fruit_skin = "Rumpled Fruit Skin" + calico_pizza = "Calico Pizza" + stuffed_mushrooms = "Stuffed Mushrooms" + elf_quesadilla = "Elf Quesadilla" + nachos_of_the_desert = "Nachos Of The Desert" + cloppino = "Cloppino" + rainforest_shrimp = "Rainforest Shrimp" + shrimp_donut = "Shrimp Donut" + smell_of_the_sea = "Smell Of The Sea" + desert_gumbo = "Desert Gumbo" + free_cactis = "Free Cactis" + monster_hunt = "Monster Hunt" + deep_dive = "Deep Dive" + treasure_hunt = "Treasure Hunt" + touch_calico_statue = "Touch A Calico Statue" + real_calico_egg_hunter = "Real Calico Egg Hunter" + willy_challenge = "Willy's Challenge" + desert_scholar = "Desert Scholar" + trout_derby_reward_pattern = "Trout Derby Reward " + squidfest_day_1_copper = "SquidFest Day 1 Copper" + squidfest_day_1_iron = "SquidFest Day 1 Iron" + squidfest_day_1_gold = "SquidFest Day 1 Gold" + squidfest_day_1_iridium = "SquidFest Day 1 Iridium" + squidfest_day_2_copper = "SquidFest Day 2 Copper" + squidfest_day_2_iron = "SquidFest Day 2 Iron" + squidfest_day_2_gold = "SquidFest Day 2 Gold" + squidfest_day_2_iridium = "SquidFest Day 2 Iridium" + diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py index cd59d749ee01..d4ee81430eb4 100644 --- a/worlds/stardew_valley/strings/fish_names.py +++ b/worlds/stardew_valley/strings/fish_names.py @@ -1,81 +1,92 @@ +all_fish = [] + + +def fish(fish_name: str) -> str: + all_fish.append(fish_name) + return fish_name + + class Fish: - albacore = "Albacore" - anchovy = "Anchovy" - angler = "Angler" - any = "Any Fish" - blob_fish = "Blobfish" - blobfish = "Blobfish" - blue_discus = "Blue Discus" - bream = "Bream" - bullhead = "Bullhead" - carp = "Carp" - catfish = "Catfish" - chub = "Chub" - clam = "Clam" - cockle = "Cockle" - crab = "Crab" - crayfish = "Crayfish" - crimsonfish = "Crimsonfish" - dorado = "Dorado" - eel = "Eel" - flounder = "Flounder" - ghostfish = "Ghostfish" - glacierfish = "Glacierfish" - glacierfish_jr = "Glacierfish Jr." - halibut = "Halibut" - herring = "Herring" - ice_pip = "Ice Pip" - largemouth_bass = "Largemouth Bass" - lava_eel = "Lava Eel" - legend = "Legend" - legend_ii = "Legend II" - lingcod = "Lingcod" - lionfish = "Lionfish" - lobster = "Lobster" - midnight_carp = "Midnight Carp" - midnight_squid = "Midnight Squid" - ms_angler = "Ms. Angler" - mussel = "Mussel" - mussel_node = "Mussel Node" - mutant_carp = "Mutant Carp" - octopus = "Octopus" - oyster = "Oyster" - perch = "Perch" - periwinkle = "Periwinkle" - pike = "Pike" - pufferfish = "Pufferfish" - radioactive_carp = "Radioactive Carp" - rainbow_trout = "Rainbow Trout" - red_mullet = "Red Mullet" - red_snapper = "Red Snapper" - salmon = "Salmon" - sandfish = "Sandfish" - sardine = "Sardine" - scorpion_carp = "Scorpion Carp" - sea_cucumber = "Sea Cucumber" - shad = "Shad" - shrimp = "Shrimp" - slimejack = "Slimejack" - smallmouth_bass = "Smallmouth Bass" - snail = "Snail" - son_of_crimsonfish = "Son of Crimsonfish" - spook_fish = "Spook Fish" - spookfish = "Spook Fish" - squid = "Squid" - stingray = "Stingray" - stonefish = "Stonefish" - sturgeon = "Sturgeon" - sunfish = "Sunfish" - super_cucumber = "Super Cucumber" - tiger_trout = "Tiger Trout" - tilapia = "Tilapia" - tuna = "Tuna" - void_salmon = "Void Salmon" - walleye = "Walleye" - woodskip = "Woodskip" + albacore = fish("Albacore") + anchovy = fish("Anchovy") + angler = fish("Angler") + any = fish("Any Fish") + blobfish = fish("Blobfish") + blue_discus = fish("Blue Discus") + bream = fish("Bream") + bullhead = fish("Bullhead") + carp = fish("Carp") + catfish = fish("Catfish") + chub = fish("Chub") + clam = fish("Clam") + cockle = fish("Cockle") + crab = fish("Crab") + crayfish = fish("Crayfish") + crimsonfish = fish("Crimsonfish") + dorado = fish("Dorado") + eel = fish("Eel") + flounder = fish("Flounder") + ghostfish = fish("Ghostfish") + goby = fish("Goby") + glacierfish = fish("Glacierfish") + glacierfish_jr = fish("Glacierfish Jr.") + halibut = fish("Halibut") + herring = fish("Herring") + ice_pip = fish("Ice Pip") + largemouth_bass = fish("Largemouth Bass") + lava_eel = fish("Lava Eel") + legend = fish("Legend") + legend_ii = fish("Legend II") + lingcod = fish("Lingcod") + lionfish = fish("Lionfish") + lobster = fish("Lobster") + midnight_carp = fish("Midnight Carp") + midnight_squid = fish("Midnight Squid") + ms_angler = fish("Ms. Angler") + mussel = fish("Mussel") + mussel_node = fish("Mussel Node") + mutant_carp = fish("Mutant Carp") + octopus = fish("Octopus") + oyster = fish("Oyster") + perch = fish("Perch") + periwinkle = fish("Periwinkle") + pike = fish("Pike") + pufferfish = fish("Pufferfish") + radioactive_carp = fish("Radioactive Carp") + rainbow_trout = fish("Rainbow Trout") + red_mullet = fish("Red Mullet") + red_snapper = fish("Red Snapper") + salmon = fish("Salmon") + sandfish = fish("Sandfish") + sardine = fish("Sardine") + scorpion_carp = fish("Scorpion Carp") + sea_cucumber = fish("Sea Cucumber") + shad = fish("Shad") + shrimp = fish("Shrimp") + slimejack = fish("Slimejack") + smallmouth_bass = fish("Smallmouth Bass") + snail = fish("Snail") + son_of_crimsonfish = fish("Son of Crimsonfish") + spook_fish = fish("Spook Fish") + spookfish = fish("Spook Fish") + squid = fish("Squid") + stingray = fish("Stingray") + stonefish = fish("Stonefish") + sturgeon = fish("Sturgeon") + sunfish = fish("Sunfish") + super_cucumber = fish("Super Cucumber") + tiger_trout = fish("Tiger Trout") + tilapia = fish("Tilapia") + tuna = fish("Tuna") + void_salmon = fish("Void Salmon") + walleye = fish("Walleye") + woodskip = fish("Woodskip") class WaterItem: + sea_jelly = "Sea Jelly" + river_jelly = "River Jelly" + cave_jelly = "Cave Jelly" seaweed = "Seaweed" green_algae = "Green Algae" white_algae = "White Algae" @@ -95,6 +106,7 @@ class Trash: class WaterChest: fishing_chest = "Fishing Chest" + golden_fishing_chest = "Golden Fishing Chest" treasure = "Treasure Chest" @@ -125,7 +137,6 @@ class SVEFish: void_eel = "Void Eel" water_grub = "Water Grub" sea_sponge = "Sea Sponge" - dulse_seaweed = "Dulse Seaweed" class DistantLandsFish: @@ -134,3 +145,13 @@ class DistantLandsFish: purple_algae = "Purple Algae" giant_horsehoe_crab = "Giant Horsehoe Crab" + +class SVEWaterItem: + dulse_seaweed = "Dulse Seaweed" + + +class ModTrash: + rusty_scrap = "Scrap Rust" + + +all_fish = tuple(all_fish) \ No newline at end of file diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py index 6e2f98fd581b..03784336d19c 100644 --- a/worlds/stardew_valley/strings/food_names.py +++ b/worlds/stardew_valley/strings/food_names.py @@ -42,6 +42,7 @@ class Meal: maki_roll = "Maki Roll" maple_bar = "Maple Bar" miners_treat = "Miner's Treat" + moss_soup = "Moss Soup" omelet = "Omelet" pale_broth = "Pale Broth" pancakes = "Pancakes" @@ -101,6 +102,18 @@ class SVEMeal: void_delight = "Void Delight" void_salmon_sushi = "Void Salmon Sushi" grampleton_orange_chicken = "Grampleton Orange Chicken" + stamina_capsule = "Stamina Capsule" + + +class TrashyMeal: + grilled_cheese = "Grilled Cheese" + fish_casserole = "Fish Casserole" + + +class ArchaeologyMeal: + diggers_delight = "Digger's Delight" + rocky_root = "Rocky Root Coffee" + ancient_jello = "Ancient Jello" class SVEBeverage: diff --git a/worlds/stardew_valley/strings/forageable_names.py b/worlds/stardew_valley/strings/forageable_names.py index 24127beb9838..c7dae8af3ce0 100644 --- a/worlds/stardew_valley/strings/forageable_names.py +++ b/worlds/stardew_valley/strings/forageable_names.py @@ -1,10 +1,26 @@ +all_edible_mushrooms = [] + + +def mushroom(name: str) -> str: + all_edible_mushrooms.append(name) + return name + + +class Mushroom: + any_edible = "Any Edible Mushroom" + chanterelle = mushroom("Chanterelle") + common = mushroom("Common Mushroom") + morel = mushroom("Morel") + purple = mushroom("Purple Mushroom") + red = "Red Mushroom" # Not in all mushrooms, as it can't be dried + magma_cap = mushroom("Magma Cap") + + class Forageable: blackberry = "Blackberry" cactus_fruit = "Cactus Fruit" cave_carrot = "Cave Carrot" - chanterelle = "Chanterelle" coconut = "Coconut" - common_mushroom = "Common Mushroom" crocus = "Crocus" crystal_fruit = "Crystal Fruit" daffodil = "Daffodil" @@ -16,8 +32,6 @@ class Forageable: holly = "Holly" journal_scrap = "Journal Scrap" leek = "Leek" - magma_cap = "Magma Cap" - morel = "Morel" secret_note = "Secret Note" spice_berry = "Spice Berry" sweet_pea = "Sweet Pea" @@ -25,8 +39,6 @@ class Forageable: wild_plum = "Wild Plum" winter_root = "Winter Root" dragon_tooth = "Dragon Tooth" - red_mushroom = "Red Mushroom" - purple_mushroom = "Purple Mushroom" rainbow_shell = "Rainbow Shell" salmonberry = "Salmonberry" snow_yam = "Snow Yam" @@ -34,28 +46,26 @@ class Forageable: class SVEForage: - ornate_treasure_chest = "Ornate Treasure Chest" - swirl_stone = "Swirl Stone" - void_pebble = "Void Pebble" - void_soul = "Void Soul" ferngill_primrose = "Ferngill Primrose" goldenrod = "Goldenrod" winter_star_rose = "Winter Star Rose" - bearberrys = "Bearberrys" + bearberry = "Bearberry" poison_mushroom = "Poison Mushroom" red_baneberry = "Red Baneberry" - big_conch = "Big Conch" + conch = "Conch" dewdrop_berry = "Dewdrop Berry" - dried_sand_dollar = "Dried Sand Dollar" + sand_dollar = "Sand Dollar" golden_ocean_flower = "Golden Ocean Flower" - lucky_four_leaf_clover = "Lucky Four Leaf Clover" + four_leaf_clover = "Four Leaf Clover" mushroom_colony = "Mushroom Colony" - poison_mushroom = "Poison Mushroom" rusty_blade = "Rusty Blade" - smelly_rafflesia = "Smelly Rafflesia" + rafflesia = "Rafflesia" thistle = "Thistle" class DistantLandsForageable: brown_amanita = "Brown Amanita" swamp_herb = "Swamp Herb" + + +all_edible_mushrooms = tuple(all_edible_mushrooms) diff --git a/worlds/stardew_valley/strings/machine_names.py b/worlds/stardew_valley/strings/machine_names.py index f9be78c41a03..d9e249a33594 100644 --- a/worlds/stardew_valley/strings/machine_names.py +++ b/worlds/stardew_valley/strings/machine_names.py @@ -1,4 +1,6 @@ class Machine: + dehydrator = "Dehydrator" + fish_smoker = "Fish Smoker" bee_house = "Bee House" bone_mill = "Bone Mill" cask = "Cask" @@ -10,6 +12,7 @@ class Machine: enricher = "Enricher" furnace = "Furnace" geode_crusher = "Geode Crusher" + mushroom_log = "Mushroom Log" heavy_tapper = "Heavy Tapper" keg = "Keg" lightning_rod = "Lightning Rod" @@ -26,4 +29,9 @@ class Machine: solar_panel = "Solar Panel" tapper = "Tapper" worm_bin = "Worm Bin" + deluxe_worm_bin = "Deluxe Worm Bin" + heavy_furnace = "Heavy Furnace" + anvil = "Anvil" + mini_forge = "Mini-Forge" + bait_maker = "Bait Maker" diff --git a/worlds/stardew_valley/strings/material_names.py b/worlds/stardew_valley/strings/material_names.py index 16511a5bcb97..797a42b73756 100644 --- a/worlds/stardew_valley/strings/material_names.py +++ b/worlds/stardew_valley/strings/material_names.py @@ -1,4 +1,5 @@ class Material: + moss = "Moss" coal = "Coal" fiber = "Fiber" hardwood = "Hardwood" diff --git a/worlds/stardew_valley/strings/metal_names.py b/worlds/stardew_valley/strings/metal_names.py index bf15b9d01c8e..7798c06defeb 100644 --- a/worlds/stardew_valley/strings/metal_names.py +++ b/worlds/stardew_valley/strings/metal_names.py @@ -44,6 +44,7 @@ class Mineral: ruby = "Ruby" emerald = "Emerald" amethyst = "Amethyst" + tigerseye = "Tigerseye" class Artifact: diff --git a/worlds/stardew_valley/strings/monster_drop_names.py b/worlds/stardew_valley/strings/monster_drop_names.py index c42e7ad5ede0..df2cacf0c6aa 100644 --- a/worlds/stardew_valley/strings/monster_drop_names.py +++ b/worlds/stardew_valley/strings/monster_drop_names.py @@ -14,4 +14,8 @@ class Loot: class ModLoot: void_shard = "Void Shard" green_mushroom = "Green Mushroom" + ornate_treasure_chest = "Ornate Treasure Chest" + swirl_stone = "Swirl Stone" + void_pebble = "Void Pebble" + void_soul = "Void Soul" diff --git a/worlds/stardew_valley/strings/quest_names.py b/worlds/stardew_valley/strings/quest_names.py index 2c02381609ec..6370b8b56875 100644 --- a/worlds/stardew_valley/strings/quest_names.py +++ b/worlds/stardew_valley/strings/quest_names.py @@ -4,6 +4,7 @@ class Quest: getting_started = "Getting Started" to_the_beach = "To The Beach" raising_animals = "Raising Animals" + feeding_animals = "Feeding Animals" advancement = "Advancement" archaeology = "Archaeology" rat_problem = "Rat Problem" @@ -49,12 +50,13 @@ class Quest: dark_talisman = "Dark Talisman" goblin_problem = "Goblin Problem" magic_ink = "Magic Ink" + giant_stump = "The Giant Stump" class ModQuest: MrGinger = "Mr.Ginger's request" AyeishaEnvelope = "Missing Envelope" - AyeishaRing = "Lost Emerald Ring" + AyeishaRing = "Ayeisha's Lost Ring" JunaCola = "Juna's Drink Request" JunaSpaghetti = "Juna's BFF Request" RailroadBoulder = "The Railroad Boulder" diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 0fdab64fef68..2bbc6228ab19 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -14,6 +14,7 @@ class Region: forest = "Forest" bus_stop = "Bus Stop" backwoods = "Backwoods" + tunnel_entrance = "Tunnel Entrance" bus_tunnel = "Bus Tunnel" railroad = "Railroad" secret_woods = "Secret Woods" @@ -28,7 +29,6 @@ class Region: oasis = "Oasis" casino = "Casino" mines = "The Mines" - mines_dwarf_shop = "Mines Dwarf Shop" skull_cavern_entrance = "Skull Cavern Entrance" skull_cavern = "Skull Cavern" sewer = "Sewer" @@ -73,17 +73,9 @@ class Region: alex_house = "Alex's House" elliott_house = "Elliott's House" ranch = "Marnie's Ranch" - traveling_cart = "Traveling Cart" - traveling_cart_sunday = "Traveling Cart Sunday" - traveling_cart_monday = "Traveling Cart Monday" - traveling_cart_tuesday = "Traveling Cart Tuesday" - traveling_cart_wednesday = "Traveling Cart Wednesday" - traveling_cart_thursday = "Traveling Cart Thursday" - traveling_cart_friday = "Traveling Cart Friday" - traveling_cart_saturday = "Traveling Cart Saturday" + mastery_cave = "Mastery Cave" farm_cave = "Farmcave" greenhouse = "Greenhouse" - tunnel_entrance = "Tunnel Entrance" leah_house = "Leah's Cottage" wizard_tower = "Wizard Tower" wizard_basement = "Wizard Basement" @@ -91,6 +83,7 @@ class Region: maru_room = "Maru's Room" sebastian_room = "Sebastian's Room" adventurer_guild = "Adventurer's Guild" + adventurer_guild_bedroom = "Marlon's bedroom" quarry = "Quarry" quarry_mine_entrance = "Quarry Mine Entrance" quarry_mine = "Quarry Mine" @@ -121,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" @@ -148,6 +142,20 @@ class Region: dangerous_mines_20 = "Dangerous Mines - Floor 20" dangerous_mines_60 = "Dangerous Mines - Floor 60" dangerous_mines_100 = "Dangerous Mines - Floor 100" + + +class LogicRegion: + mines_dwarf_shop = "Mines Dwarf Shop" + + traveling_cart = "Traveling Cart" + traveling_cart_sunday = "Traveling Cart Sunday" + traveling_cart_monday = "Traveling Cart Monday" + traveling_cart_tuesday = "Traveling Cart Tuesday" + traveling_cart_wednesday = "Traveling Cart Wednesday" + traveling_cart_thursday = "Traveling Cart Thursday" + traveling_cart_friday = "Traveling Cart Friday" + traveling_cart_saturday = "Traveling Cart Saturday" + kitchen = "Kitchen" shipping = "Shipping" queen_of_sauce = "The Queen of Sauce" @@ -155,9 +163,18 @@ class Region: blacksmith_iron = "Blacksmith Iron Upgrades" blacksmith_gold = "Blacksmith Gold Upgrades" blacksmith_iridium = "Blacksmith Iridium Upgrades" - farming = "Farming" + + spring_farming = "Spring Farming" + summer_farming = "Summer Farming" + fall_farming = "Fall Farming" + winter_farming = "Winter Farming" + indoor_farming = "Indoor Farming" + summer_or_fall_farming = "Summer or Fall Farming" + fishing = "Fishing" egg_festival = "Egg Festival" + desert_festival = "Desert Festival" + trout_derby = "Trout Derby" flower_dance = "Flower Dance" luau = "Luau" moonlight_jellies = "Dance of the Moonlight Jellies" @@ -166,6 +183,13 @@ class Region: festival_of_ice = "Festival of Ice" night_market = "Night Market" winter_star = "Feast of the Winter Star" + squidfest = "SquidFest" + raccoon_daddy = "Raccoon Bundles" + raccoon_shop = "Raccoon Shop" + bookseller_1 = "Bookseller Experience Books" + bookseller_2 = "Bookseller Year 1 Books" + bookseller_3 = "Bookseller Year 3 Books" + forest_waterfall = "Waterfall" class DeepWoodsRegion: @@ -273,6 +297,7 @@ class SVERegion: sprite_spring_cave = "Sprite Spring Cave" willy_bedroom = "Willy's Bedroom" gunther_bedroom = "Gunther's Bedroom" + highlands_pond = "Highlands Pond" class AlectoRegion: @@ -302,5 +327,3 @@ class BoardingHouseRegion: lost_valley_house_1 = "Lost Valley Ruins - First House" lost_valley_house_2 = "Lost Valley Ruins - Second House" buffalo_ranch = "Buffalo's Ranch" - - diff --git a/worlds/stardew_valley/strings/season_names.py b/worlds/stardew_valley/strings/season_names.py index f3659bc87fe0..1c4971c3f802 100644 --- a/worlds/stardew_valley/strings/season_names.py +++ b/worlds/stardew_valley/strings/season_names.py @@ -5,4 +5,5 @@ class Season: winter = "Winter" progressive = "Progressive Season" + all = (spring, summer, fall, winter) not_winter = (spring, summer, fall,) diff --git a/worlds/stardew_valley/strings/seed_names.py b/worlds/stardew_valley/strings/seed_names.py index 398b370f2745..f2799d4e449f 100644 --- a/worlds/stardew_valley/strings/seed_names.py +++ b/worlds/stardew_valley/strings/seed_names.py @@ -1,35 +1,72 @@ class Seed: + amaranth = "Amaranth Seeds" + artichoke = "Artichoke Seeds" + bean = "Bean Starter" + beet = "Beet Seeds" + blueberry = "Blueberry Seeds" + bok_choy = "Bok Choy Seeds" + broccoli = "Broccoli Seeds" + cactus = "Cactus Seeds" + carrot = "Carrot Seeds" + cauliflower = "Cauliflower Seeds" + coffee_starter = "Coffee Bean (Starter)" + """This item does not really exist and should never end up being displayed. + It's there to patch the loop in logic because "Coffee Bean" is both the seed and the crop.""" coffee = "Coffee Bean" + corn = "Corn Seeds" + cranberry = "Cranberry Seeds" + eggplant = "Eggplant Seeds" + fairy = "Fairy Seeds" garlic = "Garlic Seeds" + grape = "Grape Starter" + hops = "Hops Starter" jazz = "Jazz Seeds" + kale = "Kale Seeds" melon = "Melon Seeds" mixed = "Mixed Seeds" + mixed_flower = "Mixed Flower Seeds" + parsnip = "Parsnip Seeds" + pepper = "Pepper Seeds" pineapple = "Pineapple Seeds" poppy = "Poppy Seeds" + potato = "Potato Seeds" + powdermelon = "Powdermelon Seeds" + pumpkin = "Pumpkin Seeds" qi_bean = "Qi Bean" + radish = "Radish Seeds" + rare_seed = "Rare Seed" + red_cabbage = "Red Cabbage Seeds" + rhubarb = "Rhubarb Seeds" + rice = "Rice Shoot" spangle = "Spangle Seeds" + starfruit = "Starfruit Seeds" + strawberry = "Strawberry Seeds" + summer_squash = "Summer Squash Seeds" sunflower = "Sunflower Seeds" taro = "Taro Tuber" tomato = "Tomato Seeds" tulip = "Tulip Bulb" wheat = "Wheat Seeds" + yam = "Yam Seeds" class TreeSeed: acorn = "Acorn" maple = "Maple Seed" + mossy = "Mossy Seed" + mystic = "Mystic Tree Seed" pine = "Pine Cone" mahogany = "Mahogany Seed" mushroom = "Mushroom Tree Seed" class SVESeed: - stalk_seed = "Stalk Seed" - fungus_seed = "Fungus Seed" - slime_seed = "Slime Seed" - void_seed = "Void Seed" - shrub_seed = "Shrub Seed" - ancient_ferns_seed = "Ancient Ferns Seed" + stalk = "Stalk Seed" + fungus = "Fungus Seed" + slime = "Slime Seed" + void = "Void Seed" + shrub = "Shrub Seed" + ancient_fern = "Ancient Fern Seed" class DistantLandsSeed: diff --git a/worlds/stardew_valley/strings/skill_names.py b/worlds/stardew_valley/strings/skill_names.py index bae4c26fd716..7f3a61f2dfcd 100644 --- a/worlds/stardew_valley/strings/skill_names.py +++ b/worlds/stardew_valley/strings/skill_names.py @@ -15,4 +15,6 @@ class ModSkill: socializing = "Socializing" +all_vanilla_skills = {Skill.farming, Skill.foraging, Skill.fishing, Skill.mining, Skill.combat} all_mod_skills = {ModSkill.luck, ModSkill.binning, ModSkill.archaeology, ModSkill.cooking, ModSkill.magic, ModSkill.socializing} +all_skills = {*all_vanilla_skills, *all_mod_skills} diff --git a/worlds/stardew_valley/strings/tool_names.py b/worlds/stardew_valley/strings/tool_names.py index ea8c00b9bfd2..761f50e0a9bb 100644 --- a/worlds/stardew_valley/strings/tool_names.py +++ b/worlds/stardew_valley/strings/tool_names.py @@ -4,6 +4,7 @@ class Tool: hoe = "Hoe" watering_can = "Watering Can" trash_can = "Trash Can" + pan = "Pan" fishing_rod = "Fishing Rod" scythe = "Scythe" golden_scythe = "Golden Scythe" diff --git a/worlds/stardew_valley/strings/wallet_item_names.py b/worlds/stardew_valley/strings/wallet_item_names.py index 28f09b0558fc..32655efe88c2 100644 --- a/worlds/stardew_valley/strings/wallet_item_names.py +++ b/worlds/stardew_valley/strings/wallet_item_names.py @@ -8,3 +8,4 @@ class Wallet: skull_key = "Skull Key" dark_talisman = "Dark Talisman" club_card = "Club Card" + mastery_of_the_five_ways = "Mastery Of The Five Ways" diff --git a/worlds/stardew_valley/test/TestBooksanity.py b/worlds/stardew_valley/test/TestBooksanity.py new file mode 100644 index 000000000000..942f35d961a9 --- /dev/null +++ b/worlds/stardew_valley/test/TestBooksanity.py @@ -0,0 +1,206 @@ +from . import SVTestBase +from ..options import ExcludeGingerIsland, Booksanity, Shipsanity +from ..strings.book_names import Book, LostBook + +power_books = [Book.animal_catalogue, Book.book_of_mysteries, + Book.the_alleyway_buffet, Book.the_art_o_crabbing, Book.dwarvish_safety_manual, + Book.jewels_of_the_sea, Book.raccoon_journal, Book.woodys_secret, Book.jack_be_nimble_jack_be_thick, Book.friendship_101, + Book.monster_compendium, Book.mapping_cave_systems, Book.treasure_appraisal_guide, Book.way_of_the_wind_pt_1, Book.way_of_the_wind_pt_2, + Book.horse_the_book, Book.ol_slitherlegs, Book.price_catalogue, Book.the_diamond_hunter, ] + +skill_books = [Book.combat_quarterly, Book.woodcutters_weekly, Book.book_of_stars, Book.stardew_valley_almanac, Book.bait_and_bobber, Book.mining_monthly, + Book.queen_of_sauce_cookbook, ] + +lost_books = [ + LostBook.tips_on_farming, LostBook.this_is_a_book_by_marnie, LostBook.on_foraging, LostBook.the_fisherman_act_1, + LostBook.how_deep_do_the_mines_go, LostBook.an_old_farmers_journal, LostBook.scarecrows, LostBook.the_secret_of_the_stardrop, + LostBook.journey_of_the_prairie_king_the_smash_hit_video_game, LostBook.a_study_on_diamond_yields, LostBook.brewmasters_guide, + LostBook.mysteries_of_the_dwarves, LostBook.highlights_from_the_book_of_yoba, LostBook.marriage_guide_for_farmers, LostBook.the_fisherman_act_ii, + LostBook.technology_report, LostBook.secrets_of_the_legendary_fish, LostBook.gunther_tunnel_notice, LostBook.note_from_gunther, + LostBook.goblins_by_m_jasper, LostBook.secret_statues_acrostics, ] + +lost_book = "Progressive Lost Book" + + +class TestBooksanityNone(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_none, + } + + def test_no_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertNotIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityPowers(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_power, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_no_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityPowersAndSkills(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_power_skill, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityAll(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_all, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py index cd6828cd79e5..091f39b2568e 100644 --- a/worlds/stardew_valley/test/TestBundles.py +++ b/worlds/stardew_valley/test/TestBundles.py @@ -1,8 +1,12 @@ import unittest -from ..data.bundle_data import all_bundle_items_except_money, quality_crops_items_thematic +from . import SVTestBase +from .. import BundleRandomization +from ..data.bundle_data import all_bundle_items_except_money, quality_crops_items_thematic, quality_foraging_items, quality_fish_items +from ..options import BundlePlando +from ..strings.bundle_names import BundleName from ..strings.crop_names import Fruit -from ..strings.quality_names import CropQuality +from ..strings.quality_names import CropQuality, ForageQuality, FishQuality class TestBundles(unittest.TestCase): @@ -27,3 +31,60 @@ def test_quality_crops_have_correct_quality(self): with self.subTest(bundle_item.item_name): self.assertEqual(bundle_item.quality, CropQuality.gold) + def test_quality_foraging_have_correct_amounts(self): + for bundle_item in quality_foraging_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.amount, 3) + + def test_quality_foraging_have_correct_quality(self): + for bundle_item in quality_foraging_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.quality, ForageQuality.gold) + + def test_quality_fish_have_correct_amounts(self): + for bundle_item in quality_fish_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.amount, 2) + + def test_quality_fish_have_correct_quality(self): + for bundle_item in quality_fish_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.quality, FishQuality.gold) + + +class TestRemixedPlandoBundles(SVTestBase): + plando_bundles = {BundleName.money_2500, BundleName.money_5000, BundleName.money_10000, BundleName.gambler, BundleName.ocean_fish, + BundleName.lake_fish, BundleName.deep_fishing, BundleName.spring_fish, BundleName.legendary_fish, BundleName.bait} + options = { + BundleRandomization: BundleRandomization.option_remixed, + BundlePlando: frozenset(plando_bundles) + } + + def test_all_plando_bundles_are_there(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for bundle_name in self.plando_bundles: + with self.subTest(f"{bundle_name}"): + self.assertIn(bundle_name, location_names) + self.assertNotIn(BundleName.money_25000, location_names) + self.assertNotIn(BundleName.carnival, location_names) + self.assertNotIn(BundleName.night_fish, location_names) + self.assertNotIn(BundleName.specialty_fish, location_names) + self.assertNotIn(BundleName.specific_bait, location_names) + + +class TestRemixedAnywhereBundles(SVTestBase): + fish_bundle_names = {BundleName.spring_fish, BundleName.summer_fish, BundleName.fall_fish, BundleName.winter_fish, BundleName.ocean_fish, + BundleName.lake_fish, BundleName.river_fish, BundleName.night_fish, BundleName.legendary_fish, BundleName.specialty_fish, + BundleName.bait, BundleName.specific_bait, BundleName.crab_pot, BundleName.tackle, BundleName.quality_fish, + BundleName.rain_fish, BundleName.master_fisher} + options = { + BundleRandomization: BundleRandomization.option_remixed_anywhere, + BundlePlando: frozenset(fish_bundle_names) + } + + def test_all_plando_bundles_are_there(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for bundle_name in self.fish_bundle_names: + with self.subTest(f"{bundle_name}"): + self.assertIn(bundle_name, location_names) + diff --git a/worlds/stardew_valley/test/TestCrops.py b/worlds/stardew_valley/test/TestCrops.py index 38b736367b80..4fa836a97d14 100644 --- a/worlds/stardew_valley/test/TestCrops.py +++ b/worlds/stardew_valley/test/TestCrops.py @@ -11,10 +11,10 @@ def test_need_greenhouse_for_cactus(self): harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit") self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Cactus Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - self.multiworld.state.collect(self.world.create_item("Desert Obelisk"), event=False) + self.multiworld.state.collect(self.create_item("Cactus Seeds")) + self.multiworld.state.collect(self.create_item("Shipping Bin")) + self.multiworld.state.collect(self.create_item("Desert Obelisk")) self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Greenhouse"), event=False) + self.multiworld.state.collect(self.create_item("Greenhouse")) self.assert_rule_true(harvest_cactus, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestData.py b/worlds/stardew_valley/test/TestData.py index a77dc17319b7..86550705b917 100644 --- a/worlds/stardew_valley/test/TestData.py +++ b/worlds/stardew_valley/test/TestData.py @@ -2,19 +2,52 @@ from ..items import load_item_csv from ..locations import load_location_csv +from ..options import Mods class TestCsvIntegrity(unittest.TestCase): def test_items_integrity(self): items = load_item_csv() - for item in items: - self.assertIsNotNone(item.code_without_offset, "Some item do not have an id." - " Run the script `update_data.py` to generate them.") + with self.subTest("Test all items have an id"): + for item in items: + self.assertIsNotNone(item.code_without_offset, "Some item do not have an id." + " Run the script `update_data.py` to generate them.") + with self.subTest("Test all ids are unique"): + all_ids = [item.code_without_offset for item in items] + unique_ids = set(all_ids) + self.assertEqual(len(all_ids), len(unique_ids)) + + with self.subTest("Test all names are unique"): + all_names = [item.name for item in items] + unique_names = set(all_names) + self.assertEqual(len(all_names), len(unique_names)) + + with self.subTest("Test all mod names are valid"): + mod_names = {item.mod_name for item in items} + for mod_name in mod_names: + if mod_name: + self.assertIn(mod_name, Mods.valid_keys) def test_locations_integrity(self): locations = load_location_csv() - for location in locations: - self.assertIsNotNone(location.code_without_offset, "Some location do not have an id." - " Run the script `update_data.py` to generate them.") + with self.subTest("Test all locations have an id"): + for location in locations: + self.assertIsNotNone(location.code_without_offset, "Some location do not have an id." + " Run the script `update_data.py` to generate them.") + with self.subTest("Test all ids are unique"): + all_ids = [location.code_without_offset for location in locations] + unique_ids = set(all_ids) + self.assertEqual(len(all_ids), len(unique_ids)) + + with self.subTest("Test all names are unique"): + all_names = [location.name for location in locations] + unique_names = set(all_names) + self.assertEqual(len(all_names), len(unique_names)) + + with self.subTest("Test all mod names are valid"): + mod_names = {location.mod_name for location in locations} + for mod_name in mod_names: + if mod_name: + self.assertIn(mod_name, Mods.valid_keys) diff --git a/worlds/stardew_valley/test/TestDynamicGoals.py b/worlds/stardew_valley/test/TestDynamicGoals.py index fe1bfb5f3044..b0e6d6c62655 100644 --- a/worlds/stardew_valley/test/TestDynamicGoals.py +++ b/worlds/stardew_valley/test/TestDynamicGoals.py @@ -12,29 +12,30 @@ def collect_fishing_abilities(tester: SVTestBase): for i in range(4): - tester.multiworld.state.collect(tester.world.create_item(APTool.fishing_rod), event=False) - tester.multiworld.state.collect(tester.world.create_item(APTool.pickaxe), event=False) - tester.multiworld.state.collect(tester.world.create_item(APTool.axe), event=False) - tester.multiworld.state.collect(tester.world.create_item(APWeapon.weapon), event=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.fishing_rod), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.pickaxe), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.axe), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APWeapon.weapon), prevent_sweep=False) for i in range(10): - tester.multiworld.state.collect(tester.world.create_item("Fishing Level"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Combat Level"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Mining Level"), event=False) + tester.multiworld.state.collect(tester.world.create_item("Fishing Level"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Combat Level"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Mining Level"), prevent_sweep=False) for i in range(17): - tester.multiworld.state.collect(tester.world.create_item("Progressive Mine Elevator"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Spring"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Summer"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Fall"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Winter"), event=False) - tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), event=False) - tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), event=False) + tester.multiworld.state.collect(tester.world.create_item("Progressive Mine Elevator"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Spring"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Summer"), prevent_sweep=False) + 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) def create_and_collect(tester: SVTestBase, item_name: str) -> StardewItem: item = tester.world.create_item(item_name) - tester.multiworld.state.collect(item, event=False) + tester.multiworld.state.collect(item, prevent_sweep=False) return item diff --git a/worlds/stardew_valley/test/TestFarmType.py b/worlds/stardew_valley/test/TestFarmType.py new file mode 100644 index 000000000000..f78edc3eece8 --- /dev/null +++ b/worlds/stardew_valley/test/TestFarmType.py @@ -0,0 +1,31 @@ +from . import SVTestBase +from .assertion import WorldAssertMixin +from .. import options + + +class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase): + options = { + options.FarmType.internal_name: options.FarmType.option_standard, + } + + def test_start_inventory_progressive_coops(self): + start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player])) + items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) + self.assertIn("Progressive Coop", items) + self.assertEqual(items["Progressive Coop"], 3) + self.assertNotIn("Progressive Coop", start_items) + + +class TestStartInventoryMeadowLands(WorldAssertMixin, SVTestBase): + options = { + options.FarmType.internal_name: options.FarmType.option_meadowlands, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + } + + def test_start_inventory_progressive_coops(self): + start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player])) + items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) + self.assertIn("Progressive Coop", items) + self.assertEqual(items["Progressive Coop"], 2) + self.assertIn("Progressive Coop", start_items) + self.assertEqual(start_items["Progressive Coop"], 1) diff --git a/worlds/stardew_valley/test/TestFill.py b/worlds/stardew_valley/test/TestFill.py new file mode 100644 index 000000000000..0bfacb6ef6f5 --- /dev/null +++ b/worlds/stardew_valley/test/TestFill.py @@ -0,0 +1,30 @@ +from . import SVTestBase, minimal_locations_maximal_items +from .assertion import WorldAssertMixin +from .. import options +from ..mods.mod_data import ModNames + + +class TestMinLocationsMaxItems(WorldAssertMixin, SVTestBase): + options = minimal_locations_maximal_items() + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) + + +class TestSpecificSeedForTroubleshooting(WorldAssertMixin, SVTestBase): + options = { + options.Fishsanity: options.Fishsanity.option_all, + options.Goal: options.Goal.option_master_angler, + options.QuestLocations: -1, + options.Mods: (ModNames.sve,), + } + seed = 65453499742665118161 + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) diff --git a/worlds/stardew_valley/test/TestFishsanity.py b/worlds/stardew_valley/test/TestFishsanity.py new file mode 100644 index 000000000000..c5d87c0f8dd7 --- /dev/null +++ b/worlds/stardew_valley/test/TestFishsanity.py @@ -0,0 +1,405 @@ +import unittest +from typing import ClassVar, Set + +from . import SVTestBase +from .assertion import WorldAssertMixin +from ..content.feature import fishsanity +from ..mods.mod_data import ModNames +from ..options import Fishsanity, ExcludeGingerIsland, Mods, SpecialOrderLocations, Goal, QuestLocations +from ..strings.fish_names import Fish, SVEFish, DistantLandsFish + +pelican_town_legendary_fishes = {Fish.angler, Fish.crimsonfish, Fish.glacierfish, Fish.legend, Fish.mutant_carp, } +pelican_town_hard_special_fishes = {Fish.lava_eel, Fish.octopus, Fish.scorpion_carp, Fish.ice_pip, Fish.super_cucumber, } +pelican_town_medium_special_fishes = {Fish.blobfish, Fish.dorado, } +pelican_town_hard_normal_fishes = {Fish.lingcod, Fish.pufferfish, Fish.void_salmon, } +pelican_town_medium_normal_fishes = { + Fish.albacore, Fish.catfish, Fish.eel, Fish.flounder, Fish.ghostfish, Fish.goby, Fish.halibut, Fish.largemouth_bass, Fish.midnight_carp, + Fish.midnight_squid, Fish.pike, Fish.red_mullet, Fish.salmon, Fish.sandfish, Fish.slimejack, Fish.stonefish, Fish.spook_fish, Fish.squid, Fish.sturgeon, + Fish.tiger_trout, Fish.tilapia, Fish.tuna, Fish.woodskip, +} +pelican_town_easy_normal_fishes = { + Fish.anchovy, Fish.bream, Fish.bullhead, Fish.carp, Fish.chub, Fish.herring, Fish.perch, Fish.rainbow_trout, Fish.red_snapper, Fish.sardine, Fish.shad, + Fish.sea_cucumber, Fish.shad, Fish.smallmouth_bass, Fish.sunfish, Fish.walleye, +} +pelican_town_crab_pot_fishes = { + Fish.clam, Fish.cockle, Fish.crab, Fish.crayfish, Fish.lobster, Fish.mussel, Fish.oyster, Fish.periwinkle, Fish.shrimp, Fish.snail, +} + +ginger_island_hard_fishes = {Fish.pufferfish, Fish.stingray, Fish.super_cucumber, } +ginger_island_medium_fishes = {Fish.blue_discus, Fish.lionfish, Fish.tilapia, Fish.tuna, } +qi_board_legendary_fishes = {Fish.ms_angler, Fish.son_of_crimsonfish, Fish.glacierfish_jr, Fish.legend_ii, Fish.radioactive_carp, } + +sve_pelican_town_hard_fishes = { + SVEFish.grass_carp, SVEFish.king_salmon, SVEFish.kittyfish, SVEFish.meteor_carp, SVEFish.puppyfish, SVEFish.radioactive_bass, SVEFish.undeadfish, + SVEFish.void_eel, +} +sve_pelican_town_medium_fishes = { + SVEFish.bonefish, SVEFish.butterfish, SVEFish.frog, SVEFish.goldenfish, SVEFish.snatcher_worm, SVEFish.water_grub, +} +sve_pelican_town_easy_fishes = {SVEFish.bull_trout, SVEFish.minnow, } +sve_ginger_island_hard_fishes = {SVEFish.gemfish, SVEFish.shiny_lunaloo, } +sve_ginger_island_medium_fishes = {SVEFish.daggerfish, SVEFish.lunaloo, SVEFish.starfish, SVEFish.torpedo_trout, } +sve_ginger_island_easy_fishes = {SVEFish.baby_lunaloo, SVEFish.clownfish, SVEFish.seahorse, SVEFish.sea_sponge, } + +distant_lands_hard_fishes = {DistantLandsFish.giant_horsehoe_crab, } +distant_lands_easy_fishes = {DistantLandsFish.void_minnow, DistantLandsFish.purple_algae, DistantLandsFish.swamp_leech, } + + +def complete_options_with_default(options): + return { + **{ + ExcludeGingerIsland: ExcludeGingerIsland.default, + Mods: Mods.default, + SpecialOrderLocations: SpecialOrderLocations.default, + }, + **options + } + + +class SVFishsanityTestBase(SVTestBase): + expected_fishes: ClassVar[Set[str]] = set() + + @classmethod + def setUpClass(cls) -> None: + if cls is SVFishsanityTestBase: + raise unittest.SkipTest("Base tests disabled") + + super().setUpClass() + + def test_fishsanity(self): + with self.subTest("Locations are valid"): + self.check_all_locations_match_expected_fishes() + + def check_all_locations_match_expected_fishes(self): + location_fishes = { + name + for location_name in self.get_real_location_names() + if (name := fishsanity.extract_fish_from_location_name(location_name)) is not None + } + + self.assertEqual(location_fishes, self.expected_fishes) + + +class TestFishsanityNoneVanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_none, + }) + + @property + def run_default_tests(self) -> bool: + # None is default + return False + + +class TestFishsanityLegendaries_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_legendaries, + }) + expected_fishes = pelican_town_legendary_fishes + + +class TestFishsanityLegendaries_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_legendaries, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = pelican_town_legendary_fishes | qi_board_legendary_fishes + + +class TestFishsanitySpecial(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_special, + }) + expected_fishes = pelican_town_legendary_fishes | pelican_town_hard_special_fishes | pelican_town_medium_special_fishes + + +class TestFishsanityAll_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityAll_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + sve_pelican_town_hard_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_hard_fishes | + sve_ginger_island_medium_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + sve_pelican_town_hard_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes + ) + + +class TestFishsanityAll_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + distant_lands_hard_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityAll_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + qi_board_legendary_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityExcludeLegendaries_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_legendaries, + }) + expected_fishes = ( + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeLegendaries_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_legendaries, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeHardFishes_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeHardFishes_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_medium_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityExcludeHardFishes_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityExcludeHardFishes_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityOnlyEasyFishes_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityOnlyEasyFishes_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityOnlyEasyFishes_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityOnlyEasyFishes_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityMasterAnglerSVEWithoutQuests(WorldAssertMixin, SVTestBase): + options = { + Fishsanity: Fishsanity.option_all, + Goal: Goal.option_master_angler, + QuestLocations: -1, + Mods: (ModNames.sve,), + } + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) diff --git a/worlds/stardew_valley/test/TestFriendsanity.py b/worlds/stardew_valley/test/TestFriendsanity.py new file mode 100644 index 000000000000..842c0edd0980 --- /dev/null +++ b/worlds/stardew_valley/test/TestFriendsanity.py @@ -0,0 +1,159 @@ +import unittest +from collections import Counter +from typing import ClassVar, Set + +from . import SVTestBase +from ..content.feature import friendsanity +from ..options import Friendsanity, FriendsanityHeartSize + +all_vanilla_bachelor = { + "Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", "Maru" +} + +all_vanilla_starting_npc = { + "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn", + "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet", +} + +all_vanilla_npc = { + "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn", + "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet", "Sandy", "Dwarf", "Kent", "Leo", + "Krobus" +} + + +class SVFriendsanityTestBase(SVTestBase): + expected_npcs: ClassVar[Set[str]] = set() + expected_pet_heart_size: ClassVar[Set[str]] = set() + expected_bachelor_heart_size: ClassVar[Set[str]] = set() + expected_other_heart_size: ClassVar[Set[str]] = set() + + @classmethod + def setUpClass(cls) -> None: + if cls is SVFriendsanityTestBase: + raise unittest.SkipTest("Base tests disabled") + + super().setUpClass() + + def test_friendsanity(self): + with self.subTest("Items are valid"): + self.check_all_items_match_expected_npcs() + with self.subTest("Correct number of items"): + self.check_correct_number_of_items() + with self.subTest("Locations are valid"): + self.check_all_locations_match_expected_npcs() + with self.subTest("Locations heart size are valid"): + self.check_all_locations_match_heart_size() + + def check_all_items_match_expected_npcs(self): + npc_names = { + name + for item in self.multiworld.itempool + if (name := friendsanity.extract_npc_from_item_name(item.name)) is not None + } + + self.assertEqual(npc_names, self.expected_npcs) + + def check_correct_number_of_items(self): + item_by_npc = Counter() + for item in self.multiworld.itempool: + name = friendsanity.extract_npc_from_item_name(item.name) + if name is None: + continue + + item_by_npc[name] += 1 + + for name, count in item_by_npc.items(): + + if name == "Pet": + self.assertEqual(count, len(self.expected_pet_heart_size)) + elif self.world.content.villagers[name].bachelor: + self.assertEqual(count, len(self.expected_bachelor_heart_size)) + else: + self.assertEqual(count, len(self.expected_other_heart_size)) + + def check_all_locations_match_expected_npcs(self): + npc_names = { + name_and_heart[0] + for location_name in self.get_real_location_names() + if (name_and_heart := friendsanity.extract_npc_from_location_name(location_name))[0] is not None + } + + self.assertEqual(npc_names, self.expected_npcs) + + def check_all_locations_match_heart_size(self): + for location_name in self.get_real_location_names(): + name, heart_size = friendsanity.extract_npc_from_location_name(location_name) + if name is None: + continue + + if name == "Pet": + self.assertIn(heart_size, self.expected_pet_heart_size) + elif self.world.content.villagers[name].bachelor: + self.assertIn(heart_size, self.expected_bachelor_heart_size) + else: + self.assertIn(heart_size, self.expected_other_heart_size) + + +class TestFriendsanityNone(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_none, + } + + @property + def run_default_tests(self) -> bool: + # None is default + return False + + +class TestFriendsanityBachelors(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_bachelors, + FriendsanityHeartSize: 1, + } + expected_npcs = all_vanilla_bachelor + expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8} + + +class TestFriendsanityStartingNpcs(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_starting_npcs, + FriendsanityHeartSize: 1, + } + expected_npcs = all_vanilla_starting_npc + expected_pet_heart_size = {1, 2, 3, 4, 5} + expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8} + expected_other_heart_size = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + +class TestFriendsanityAllNpcs(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all, + FriendsanityHeartSize: 4, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {4, 5} + expected_bachelor_heart_size = {4, 8} + expected_other_heart_size = {4, 8, 10} + + +class TestFriendsanityHeartSize3(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize: 3, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {3, 5} + expected_bachelor_heart_size = {3, 6, 9, 12, 14} + expected_other_heart_size = {3, 6, 9, 10} + + +class TestFriendsanityHeartSize5(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize: 5, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {5} + expected_bachelor_heart_size = {5, 10, 14} + expected_other_heart_size = {5, 10} diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 1b4d1476b900..56f338fe8e11 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,26 +1,27 @@ from typing import List from BaseClasses import ItemClassification, Item -from . import SVTestBase, allsanity_options_without_mods, \ - allsanity_options_with_mods, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_options +from . import SVTestBase from .. import items, location_table, options -from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name -from ..items import Group, item_table +from ..items import Group from ..locations import LocationTags -from ..mods.mod_data import ModNames from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, ToolProgression, \ - FriendsanityHeartSize + SkillProgression, Booksanity, Walnutsanity from ..strings.region_names import Region class TestBaseItemGeneration(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, Shipsanity.internal_name: Shipsanity.option_everything, Chefsanity.internal_name: Chefsanity.option_all, Craftsanity.internal_name: Craftsanity.option_all, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, } def test_all_progression_items_are_added_to_the_pool(self): @@ -34,7 +35,7 @@ def test_all_progression_items_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore] + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): self.assertIn(progression_item.name, all_created_items) @@ -65,12 +66,14 @@ def test_does_not_create_exactly_two_items(self): class TestNoGingerIslandItemGeneration(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, Shipsanity.internal_name: Shipsanity.option_everything, Chefsanity.internal_name: Chefsanity.option_all, Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + Booksanity.internal_name: Booksanity.option_all, } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -83,7 +86,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self): items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON]) items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore] + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): if Group.GINGER_ISLAND in progression_item.groups: @@ -117,7 +120,16 @@ def test_does_not_create_exactly_two_items(self): class TestMonstersanityNone(SVTestBase): - options = {options.Monstersanity.internal_name: options.Monstersanity.option_none} + options = { + options.Monstersanity.internal_name: options.Monstersanity.option_none, + # Not really necessary, but it adds more locations, so we don't have to remove useful items. + options.Fishsanity.internal_name: options.Fishsanity.option_all + } + + @property + def run_default_tests(self) -> bool: + # None is default + return False def test_when_generate_world_then_5_generic_weapons_in_the_pool(self): item_pool = [item.name for item in self.multiworld.itempool] @@ -367,408 +379,15 @@ def generate_items_for_skull_100(self) -> List[Item]: return [*combat_levels, *mining_levels, *pickaxes, *swords, bus, skull_key] -class TestLocationGeneration(SVTestBase): - - def test_all_location_created_are_in_location_table(self): - for location in self.get_real_locations(): - self.assertIn(location.name, location_table) - - -class TestMinLocationAndMaxItem(SVTestBase): - options = minimal_locations_maximal_items() - - # They do not pass and I don't know why. - skip_base_tests = True - - def test_minimal_location_maximal_items_still_valid(self): - valid_locations = self.get_real_locations() - number_locations = len(valid_locations) - number_items = len([item for item in self.multiworld.itempool - if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) - self.assertGreaterEqual(number_locations, number_items) - print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]") - - -class TestMinLocationAndMaxItemWithIsland(SVTestBase): - options = minimal_locations_maximal_items_with_island() - - def test_minimal_location_maximal_items_with_island_still_valid(self): - valid_locations = self.get_real_locations() - number_locations = len(valid_locations) - number_items = len([item for item in self.multiworld.itempool - if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) - self.assertGreaterEqual(number_locations, number_items) - print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]") - - -class TestMinSanityHasAllExpectedLocations(SVTestBase): - options = get_minsanity_options() - - def test_minsanity_has_fewer_than_locations(self): - expected_locations = 76 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertLessEqual(number_locations, expected_locations) - print(f"Stardew Valley - Minsanity Locations: {number_locations}") - if number_locations != expected_locations: - print(f"\tDisappeared Locations Detected!" - f"\n\tPlease update test_minsanity_has_fewer_than_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestDefaultSettingsHasAllExpectedLocations(SVTestBase): - options = default_options() - - def test_default_settings_has_exactly_locations(self): - expected_locations = 422 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - print(f"Stardew Valley - Default options locations: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_default_settings_has_exactly_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase): - options = allsanity_options_without_mods() - - def test_allsanity_without_mods_has_at_least_locations(self): - expected_locations = 1956 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertGreaterEqual(number_locations, expected_locations) - print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase): - options = allsanity_options_with_mods() - - def test_allsanity_with_mods_has_at_least_locations(self): - expected_locations = 2804 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertGreaterEqual(number_locations, expected_locations) - print(f"\nStardew Valley - Allsanity Locations with all mods: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestFriendsanityNone(SVTestBase): +class TestShipsanityNone(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_none, + Shipsanity.internal_name: Shipsanity.option_none } - @property def run_default_tests(self) -> bool: # None is default return False - def test_friendsanity_none(self): - with self.subTest("No Items"): - self.check_no_friendsanity_items() - with self.subTest("No Locations"): - self.check_no_friendsanity_locations() - - def check_no_friendsanity_items(self): - for item in self.multiworld.itempool: - self.assertFalse(item.name.endswith(" <3")) - - def check_no_friendsanity_locations(self): - for location_name in self.get_real_location_names(): - self.assertFalse(location_name.startswith("Friendsanity")) - - -class TestFriendsanityBachelors(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_bachelors, - FriendsanityHeartSize.internal_name: 1, - } - bachelors = {"Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", - "Maru"} - - def test_friendsanity_only_bachelors(self): - with self.subTest("Items are valid"): - self.check_only_bachelors_items() - with self.subTest("Locations are valid"): - self.check_only_bachelors_locations() - - def check_only_bachelors_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertIn(villager_name, self.bachelors) - - def check_only_bachelors_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertIn(name, self.bachelors) - self.assertLessEqual(int(hearts), 8) - - -class TestFriendsanityStartingNpcs(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_starting_npcs, - FriendsanityHeartSize.internal_name: 1, - } - excluded_npcs = {"Leo", "Krobus", "Dwarf", "Sandy", "Kent"} - - def test_friendsanity_only_starting_npcs(self): - with self.subTest("Items are valid"): - self.check_only_starting_npcs_items() - with self.subTest("Locations are valid"): - self.check_only_starting_npcs_locations() - - def check_only_starting_npcs_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertNotIn(villager_name, self.excluded_npcs) - - def check_only_starting_npcs_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertNotIn(name, self.excluded_npcs) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertLessEqual(int(hearts), 5) - elif all_villagers_by_name[name].bachelor: - self.assertLessEqual(int(hearts), 8) - else: - self.assertLessEqual(int(hearts), 10) - - -class TestFriendsanityAllNpcs(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - } - - def test_friendsanity_all_npcs(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 2) - else: - self.assertEqual(number_heart_items, 3) - self.assertEqual(item_names.count("Pet <3"), 2) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 4 or hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14) - else: - self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10) - - -class TestFriendsanityAllNpcsExcludingGingerIsland(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true - } - - def test_friendsanity_all_npcs_exclude_island(self): - with self.subTest("Items"): - self.check_items() - with self.subTest("Locations"): - self.check_locations() - - def check_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertNotEqual(villager_name, "Leo") - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertNotEqual(name, "Leo") - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertLessEqual(int(hearts), 5) - elif all_villagers_by_name[name].bachelor: - self.assertLessEqual(int(hearts), 8) - else: - self.assertLessEqual(int(hearts), 10) - - -class TestFriendsanityHeartSize3(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 3, - } - - def test_friendsanity_all_npcs_with_marriage(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 5) - else: - self.assertEqual(number_heart_items, 4) - self.assertEqual(item_names.count("Pet <3"), 2) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 3 or hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 12 or hearts == 14) - else: - self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 10) - - -class TestFriendsanityHeartSize5(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 5, - } - - def test_friendsanity_all_npcs_with_marriage(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 3) - else: - self.assertEqual(number_heart_items, 2) - self.assertEqual(item_names.count("Pet <3"), 1) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 5 or hearts == 10 or hearts == 14) - else: - self.assertTrue(hearts == 5 or hearts == 10) - - -class TestShipsanityNone(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_none - } - def test_no_shipsanity_locations(self): for location in self.get_real_locations(): with self.subTest(location.name): @@ -779,6 +398,7 @@ def test_no_shipsanity_locations(self): class TestShipsanityCrops(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_crops, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -825,7 +445,7 @@ def test_only_mainland_crop_shipsanity_locations(self): class TestShipsanityCropsNoQiCropWithoutSpecialOrders(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_crops, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_crop_shipsanity_locations(self): @@ -848,6 +468,7 @@ def test_island_crops_without_qi_fruit_shipsanity_locations(self): class TestShipsanityFish(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_fish, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -896,7 +517,7 @@ def test_exclude_island_fish_shipsanity_locations(self): class TestShipsanityFishExcludeQiOrders(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_fish, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_fish_shipsanity_locations(self): @@ -920,6 +541,7 @@ def test_include_island_fish_no_extended_family_shipsanity_locations(self): class TestShipsanityFullShipment(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -973,7 +595,7 @@ def test_exclude_island_items_shipsanity_locations(self): class TestShipsanityFullShipmentExcludeQiBoard(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla } def test_only_full_shipment_shipsanity_locations(self): @@ -1000,6 +622,7 @@ def test_exclude_qi_board_items_shipsanity_locations(self): class TestShipsanityFullShipmentWithFish(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -1069,7 +692,7 @@ def test_exclude_island_items_shipsanity_locations(self): class TestShipsanityFullShipmentWithFishExcludeQiBoard(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_full_shipment_and_fish_shipsanity_locations(self): diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py index 48bc1b152138..671fe6387258 100644 --- a/worlds/stardew_valley/test/TestItems.py +++ b/worlds/stardew_valley/test/TestItems.py @@ -1,16 +1,11 @@ -import sys -import random -import sys - from BaseClasses import MultiWorld, get_seed -from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods, get_minsanity_options +from . import setup_solo_multiworld, SVTestCase, allsanity_no_mods_6_x_x, get_minsanity_options, solo_multiworld from .. import StardewValleyWorld from ..items import Group, item_table from ..options import Friendsanity, SeasonRandomization, Museumsanity, Shipsanity, Goal from ..strings.wallet_item_names import Wallet all_seasons = ["Spring", "Summer", "Fall", "Winter"] -all_farms = ["Standard Farm", "Riverland Farm", "Forest Farm", "Hill-top Farm", "Wilderness Farm", "Four Corners Farm", "Beach Farm"] class TestItems(SVTestCase): @@ -48,16 +43,16 @@ def test_babies_come_in_all_shapes_and_sizes(self): self.assertEqual(len(baby_permutations), 4) def test_correct_number_of_stardrops(self): - allsanity_options = allsanity_options_without_mods() - multiworld = setup_solo_multiworld(allsanity_options) - stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name] - self.assertEqual(len(stardrop_items), 7) + allsanity_options = allsanity_no_mods_6_x_x() + with solo_multiworld(allsanity_options) as (multiworld, _): + stardrop_items = [item for item in multiworld.get_items() if item.name == "Stardrop"] + self.assertEqual(len(stardrop_items), 7) def test_no_duplicate_rings(self): - allsanity_options = allsanity_options_without_mods() - multiworld = setup_solo_multiworld(allsanity_options) - ring_items = [item.name for item in multiworld.get_items() if Group.RING in item_table[item.name].groups] - self.assertEqual(len(ring_items), len(set(ring_items))) + allsanity_options = allsanity_no_mods_6_x_x() + with solo_multiworld(allsanity_options) as (multiworld, _): + ring_items = [item.name for item in multiworld.get_items() if Group.RING in item_table[item.name].groups] + self.assertEqual(len(ring_items), len(set(ring_items))) def test_can_start_in_any_season(self): starting_seasons_rolled = set() @@ -75,66 +70,54 @@ def test_can_start_in_any_season(self): starting_seasons_rolled.add(f"{starting_season_items[0]}") self.assertEqual(len(starting_seasons_rolled), 4) - def test_can_start_on_any_farm(self): - starting_farms_rolled = set() - for attempt_number in range(60): - if len(starting_farms_rolled) >= 7: - print(f"Already got all 7 farm types, breaking early [{attempt_number} generations]") - break - seed = random.randrange(sys.maxsize) - multiworld = setup_solo_multiworld(seed=seed, _cache={}) - starting_farm = multiworld.worlds[1].fill_slot_data()["farm_type"] - starting_farms_rolled.add(starting_farm) - self.assertEqual(len(starting_farms_rolled), 7) - class TestMetalDetectors(SVTestCase): def test_minsanity_1_metal_detector(self): options = get_minsanity_options() - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 1) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 1) def test_museumsanity_2_metal_detector(self): options = get_minsanity_options().copy() options[Museumsanity.internal_name] = Museumsanity.option_all - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_shipsanity_full_shipment_1_metal_detector(self): options = get_minsanity_options().copy() options[Shipsanity.internal_name] = Shipsanity.option_full_shipment - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 1) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 1) def test_shipsanity_everything_2_metal_detector(self): options = get_minsanity_options().copy() options[Shipsanity.internal_name] = Shipsanity.option_everything - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_complete_collection_2_metal_detector(self): options = get_minsanity_options().copy() options[Goal.internal_name] = Goal.option_complete_collection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_perfection_2_metal_detector(self): options = get_minsanity_options().copy() options[Goal.internal_name] = Goal.option_perfection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_maxsanity_4_metal_detector(self): options = get_minsanity_options().copy() options[Museumsanity.internal_name] = Museumsanity.option_all options[Shipsanity.internal_name] = Shipsanity.option_everything options[Goal.internal_name] = Goal.option_perfection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 4) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 4) diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index 84d38ffeb449..da00a0f43e43 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -1,90 +1,105 @@ -from unittest import TestCase +import typing +import unittest +from unittest import TestCase, SkipTest -from . import setup_solo_multiworld, allsanity_options_with_mods -from .assertion import RuleAssertMixin +from BaseClasses import MultiWorld +from . import RuleAssertMixin, setup_solo_multiworld, allsanity_mods_6_x_x, minimal_locations_maximal_items +from .. import StardewValleyWorld from ..data.bundle_data import all_bundle_items_except_money - -multi_world = setup_solo_multiworld(allsanity_options_with_mods(), _cache={}) -world = multi_world.worlds[1] -logic = world.logic +from ..logic.logic import StardewLogic +from ..options import BundleRandomization def collect_all(mw): for item in mw.get_items(): - mw.state.collect(item, event=True) + mw.state.collect(item, prevent_sweep=True) + +class LogicTestBase(RuleAssertMixin, TestCase): + options: typing.Dict[str, typing.Any] = {} + multiworld: MultiWorld + logic: StardewLogic + world: StardewValleyWorld -collect_all(multi_world) + @classmethod + def setUpClass(cls) -> None: + if cls is LogicTestBase: + raise SkipTest("Not running test on base class.") + def setUp(self) -> None: + self.multiworld = setup_solo_multiworld(self.options, _cache={}) + collect_all(self.multiworld) + self.world = typing.cast(StardewValleyWorld, self.multiworld.worlds[1]) + self.logic = self.world.logic -class TestLogic(RuleAssertMixin, TestCase): def test_given_bundle_item_then_is_available_in_logic(self): for bundle_item in all_bundle_items_except_money: + if not bundle_item.can_appear(self.world.content, self.world.options): + continue + with self.subTest(msg=bundle_item.item_name): - self.assertIn(bundle_item.item_name, logic.registry.item_rules) + self.assertIn(bundle_item.get_item(), self.logic.registry.item_rules) def test_given_item_rule_then_can_be_resolved(self): - for item in logic.registry.item_rules.keys(): + for item in self.logic.registry.item_rules.keys(): with self.subTest(msg=item): - rule = logic.registry.item_rules[item] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.item_rules[item] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_building_rule_then_can_be_resolved(self): - for building in logic.registry.building_rules.keys(): + for building in self.logic.registry.building_rules.keys(): with self.subTest(msg=building): - rule = logic.registry.building_rules[building] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.building_rules[building] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_quest_rule_then_can_be_resolved(self): - for quest in logic.registry.quest_rules.keys(): + for quest in self.logic.registry.quest_rules.keys(): with self.subTest(msg=quest): - rule = logic.registry.quest_rules[quest] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.quest_rules[quest] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_special_order_rule_then_can_be_resolved(self): - for special_order in logic.registry.special_order_rules.keys(): + for special_order in self.logic.registry.special_order_rules.keys(): with self.subTest(msg=special_order): - rule = logic.registry.special_order_rules[special_order] - self.assert_rule_can_be_resolved(rule, multi_world.state) - - def test_given_tree_fruit_rule_then_can_be_resolved(self): - for tree_fruit in logic.registry.tree_fruit_rules.keys(): - with self.subTest(msg=tree_fruit): - rule = logic.registry.tree_fruit_rules[tree_fruit] - self.assert_rule_can_be_resolved(rule, multi_world.state) - - def test_given_seed_rule_then_can_be_resolved(self): - for seed in logic.registry.seed_rules.keys(): - with self.subTest(msg=seed): - rule = logic.registry.seed_rules[seed] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.special_order_rules[special_order] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_crop_rule_then_can_be_resolved(self): - for crop in logic.registry.crop_rules.keys(): + for crop in self.logic.registry.crop_rules.keys(): with self.subTest(msg=crop): - rule = logic.registry.crop_rules[crop] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.crop_rules[crop] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_fish_rule_then_can_be_resolved(self): - for fish in logic.registry.fish_rules.keys(): + for fish in self.logic.registry.fish_rules.keys(): with self.subTest(msg=fish): - rule = logic.registry.fish_rules[fish] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.fish_rules[fish] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_museum_rule_then_can_be_resolved(self): - for donation in logic.registry.museum_rules.keys(): + for donation in self.logic.registry.museum_rules.keys(): with self.subTest(msg=donation): - rule = logic.registry.museum_rules[donation] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.museum_rules[donation] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_cooking_rule_then_can_be_resolved(self): - for cooking_rule in logic.registry.cooking_rules.keys(): + for cooking_rule in self.logic.registry.cooking_rules.keys(): with self.subTest(msg=cooking_rule): - rule = logic.registry.cooking_rules[cooking_rule] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.cooking_rules[cooking_rule] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_location_rule_then_can_be_resolved(self): - for location in multi_world.get_locations(1): + for location in self.multiworld.get_locations(1): with self.subTest(msg=location.name): rule = location.access_rule - self.assert_rule_can_be_resolved(rule, multi_world.state) + self.assert_rule_can_be_resolved(rule, self.multiworld.state) + + +class TestAllSanityLogic(LogicTestBase): + options = allsanity_mods_6_x_x() + + +@unittest.skip("This test does not pass because some content is still not in content packs.") +class TestMinLocationsMaxItemsLogic(LogicTestBase): + options = minimal_locations_maximal_items() + options[BundleRandomization.internal_name] = BundleRandomization.default diff --git a/worlds/stardew_valley/test/TestMultiplePlayers.py b/worlds/stardew_valley/test/TestMultiplePlayers.py index 39be7d6f7ab2..2f2092fdf7b6 100644 --- a/worlds/stardew_valley/test/TestMultiplePlayers.py +++ b/worlds/stardew_valley/test/TestMultiplePlayers.py @@ -19,7 +19,7 @@ def test_different_festival_settings(self): multiworld = setup_multiworld(multiplayer_options) self.check_location_rule(multiworld, 1, FestivalCheck.egg_hunt, False) - self.check_location_rule(multiworld, 2, FestivalCheck.egg_hunt, True, False) + self.check_location_rule(multiworld, 2, FestivalCheck.egg_hunt, True, True) self.check_location_rule(multiworld, 3, FestivalCheck.egg_hunt, True, True) def test_different_money_settings(self): diff --git a/worlds/stardew_valley/test/TestNumberLocations.py b/worlds/stardew_valley/test/TestNumberLocations.py new file mode 100644 index 000000000000..ef552c10e8d5 --- /dev/null +++ b/worlds/stardew_valley/test/TestNumberLocations.py @@ -0,0 +1,98 @@ +from . import SVTestBase, allsanity_no_mods_6_x_x, \ + allsanity_mods_6_x_x, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_6_x_x +from .. import location_table +from ..items import Group, item_table + + +class TestLocationGeneration(SVTestBase): + + def test_all_location_created_are_in_location_table(self): + for location in self.get_real_locations(): + self.assertIn(location.name, location_table) + + +class TestMinLocationAndMaxItem(SVTestBase): + options = minimal_locations_maximal_items() + + def test_minimal_location_maximal_items_still_valid(self): + valid_locations = self.get_real_locations() + number_locations = len(valid_locations) + number_items = len([item for item in self.multiworld.itempool + if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) + print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]") + self.assertGreaterEqual(number_locations, number_items) + + +class TestMinLocationAndMaxItemWithIsland(SVTestBase): + options = minimal_locations_maximal_items_with_island() + + def test_minimal_location_maximal_items_with_island_still_valid(self): + valid_locations = self.get_real_locations() + number_locations = len(valid_locations) + number_items = len([item for item in self.multiworld.itempool + if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) + print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]") + self.assertGreaterEqual(number_locations, number_items) + + +class TestMinSanityHasAllExpectedLocations(SVTestBase): + options = get_minsanity_options() + + def test_minsanity_has_fewer_than_locations(self): + expected_locations = 85 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Minsanity Locations: {number_locations}") + self.assertLessEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tDisappeared Locations Detected!" + f"\n\tPlease update test_minsanity_has_fewer_than_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestDefaultSettingsHasAllExpectedLocations(SVTestBase): + options = default_6_x_x() + + def test_default_settings_has_exactly_locations(self): + expected_locations = 491 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Default options locations: {number_locations}") + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_default_settings_has_exactly_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase): + options = allsanity_no_mods_6_x_x() + + def test_allsanity_without_mods_has_at_least_locations(self): + expected_locations = 2238 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}") + self.assertGreaterEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase): + options = allsanity_mods_6_x_x() + + def test_allsanity_with_mods_has_at_least_locations(self): + expected_locations = 3096 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Allsanity Locations with all mods: {number_locations}") + self.assertGreaterEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index d13f9b8a051a..2cd83f013ae5 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,12 +1,13 @@ import itertools from Options import NamedRange -from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods +from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld from .assertion import WorldAssertMixin from .long.option_names import all_option_choices from .. import items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table -from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations +from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations, \ + SkillProgression from ..strings.goal_names import Goal as GoalName from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder @@ -24,7 +25,7 @@ def test_given_special_range_when_generate_then_basic_checks(self): continue for value in option.special_range_names: world_options = {option_name: option.special_range_names[value]} - with self.solo_world_sub_test(f"{option_name}: {value}", world_options, dirty_state=True) as (multiworld, _): + with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _): self.assert_basic_checks(multiworld) def test_given_choice_when_generate_then_basic_checks(self): @@ -34,7 +35,7 @@ def test_given_choice_when_generate_then_basic_checks(self): continue for value in option.options: world_options = {option_name: option.options[value]} - with self.solo_world_sub_test(f"{option_name}: {value}", world_options, dirty_state=True) as (multiworld, _): + with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _): self.assert_basic_checks(multiworld) @@ -57,63 +58,76 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self): class TestSeasonRandomization(SVTestCase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - precollected_items = {item.name for item in multi_world.precollected_items[1]} - self.assertTrue(all([season in precollected_items for season in SEASONS])) + with solo_multiworld(world_options) as (multi_world, _): + precollected_items = {item.name for item in multi_world.precollected_items[1]} + self.assertTrue(all([season in precollected_items for season in SEASONS])) def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_randomized} - multi_world = setup_solo_multiworld(world_options) - precollected_items = {item.name for item in multi_world.precollected_items[1]} - items = {item.name for item in multi_world.get_items()} | precollected_items - self.assertTrue(all([season in items for season in SEASONS])) - self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) + with solo_multiworld(world_options) as (multi_world, _): + precollected_items = {item.name for item in multi_world.precollected_items[1]} + items = {item.name for item in multi_world.get_items()} | precollected_items + self.assertTrue(all([season in items for season in SEASONS])) + self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - items = [item.name for item in multi_world.get_items()] - self.assertEqual(items.count(Season.progressive), 3) + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + self.assertEqual(items.count(Season.progressive), 3) class TestToolProgression(SVTestCase): def test_given_vanilla_when_generate_then_no_tool_in_pool(self): world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} - multi_world = setup_solo_multiworld(world_options) - - items = {item.name for item in multi_world.get_items()} - for tool in TOOLS: - self.assertNotIn(tool, items) - - def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): - world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - items = [item.name for item in multi_world.get_items()] - for tool in TOOLS: - self.assertEqual(items.count("Progressive " + tool), 4) + with solo_multiworld(world_options) as (multi_world, _): + items = {item.name for item in multi_world.get_items()} + for tool in TOOLS: + self.assertNotIn(tool, items) + + def test_given_progressive_when_generate_then_each_tool_is_in_pool_4_times(self): + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive} + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + count = items.count("Progressive " + tool) + self.assertEqual(count, 4, f"Progressive {tool} was there {count} times") + scythe_count = items.count("Progressive Scythe") + self.assertEqual(scythe_count, 1, f"Progressive Scythe was there {scythe_count} times") + self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated") + + def test_given_progressive_with_masteries_when_generate_then_fishing_rod_is_in_the_pool_5_times(self): + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries} + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + count = items.count("Progressive " + tool) + expected_count = 5 if tool == "Fishing Rod" else 4 + self.assertEqual(count, expected_count, f"Progressive {tool} was there {count} times") + scythe_count = items.count("Progressive Scythe") + self.assertEqual(scythe_count, 2, f"Progressive Scythe was there {scythe_count} times") + self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated") def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - locations = {locations.name for locations in multi_world.get_locations(1)} - for material, tool in itertools.product(ToolMaterial.tiers.values(), - [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): - if material == ToolMaterial.basic: - continue - self.assertIn(f"{material} {tool} Upgrade", locations) - self.assertIn("Purchase Training Rod", locations) - self.assertIn("Bamboo Pole Cutscene", locations) - self.assertIn("Purchase Fiberglass Rod", locations) - self.assertIn("Purchase Iridium Rod", locations) + with solo_multiworld(world_options) as (multi_world, _): + locations = {locations.name for locations in multi_world.get_locations(1)} + for material, tool in itertools.product(ToolMaterial.tiers.values(), + [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): + if material == ToolMaterial.basic: + continue + self.assertIn(f"{material} {tool} Upgrade", locations) + self.assertIn("Purchase Training Rod", locations) + self.assertIn("Bamboo Pole Cutscene", locations) + self.assertIn("Purchase Fiberglass Rod", locations) + self.assertIn("Purchase Iridium Rod", locations) class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase): - def test_given_choice_when_generate_exclude_ginger_island(self): + def test_given_choice_when_generate_exclude_ginger_island_then_ginger_island_is_properly_excluded(self): for option, option_choice in all_option_choices: if option is ExcludeGingerIsland: continue @@ -123,7 +137,7 @@ def test_given_choice_when_generate_exclude_ginger_island(self): option: option_choice } - with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options, dirty_state=True) as (multiworld, stardew_world): + with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options) as (multiworld, stardew_world): # Some options, like goals, will force Ginger island back in the game. We want to skip testing those. if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true: @@ -132,93 +146,80 @@ def test_given_choice_when_generate_exclude_ginger_island(self): self.assert_basic_checks(multiworld) self.assert_no_ginger_island_content(multiworld) - def test_given_island_related_goal_then_override_exclude_ginger_island(self): - island_goals = ["greatest_walnut_hunter", "perfection"] - for goal, exclude_island in itertools.product(island_goals, ExcludeGingerIsland.options): - world_options = { - Goal: goal, - ExcludeGingerIsland: exclude_island - } - - with self.solo_world_sub_test(f"Goal: {goal}, {ExcludeGingerIsland.internal_name}: {exclude_island}", world_options, dirty_state=True) \ - as (multiworld, stardew_world): - self.assertEqual(stardew_world.options.exclude_ginger_island, ExcludeGingerIsland.option_false) - self.assert_basic_checks(multiworld) - class TestTraps(SVTestCase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): - world_options = allsanity_options_without_mods().copy() + world_options = allsanity_no_mods_6_x_x().copy() world_options[TrapItems.internal_name] = TrapItems.option_no_traps - multi_world = setup_solo_multiworld(world_options) - - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] - multiworld_items = [item.name for item in multi_world.get_items()] + with solo_multiworld(world_options) as (multi_world, _): + trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] + multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"{item}"): - self.assertNotIn(item, multiworld_items) + for item in trap_items: + with self.subTest(f"{item}"): + self.assertNotIn(item, multiworld_items) def test_given_traps_when_generate_then_all_traps_in_pool(self): trap_option = TrapItems for value in trap_option.options: if value == "no_traps": continue - world_options = allsanity_options_with_mods() + world_options = allsanity_mods_6_x_x() world_options.update({TrapItems.internal_name: trap_option.options[value]}) - multi_world = setup_solo_multiworld(world_options) - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] - multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"Option: {value}, Item: {item}"): - self.assertIn(item, multiworld_items) + with solo_multiworld(world_options) as (multi_world, _): + trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if + Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] + multiworld_items = [item.name for item in multi_world.get_items()] + for item in trap_items: + with self.subTest(f"Option: {value}, Item: {item}"): + self.assertIn(item, multiworld_items) class TestSpecialOrders(SVTestCase): def test_given_disabled_then_no_order_in_pool(self): - world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} - for location_name in locations_in_pool: - location = location_table[location_name] - self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags) - self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla} + with solo_multiworld(world_options) as (multi_world, _): + locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} + for location_name in locations_in_pool: + location = location_table[location_name] + self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags) + self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) def test_given_board_only_then_no_qi_order_in_pool(self): - world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only} - multi_world = setup_solo_multiworld(world_options) + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board} + with solo_multiworld(world_options) as (multi_world, _): - locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} - for location_name in locations_in_pool: - location = location_table[location_name] - self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) + locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} + for location_name in locations_in_pool: + location = location_table[location_name] + self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) - for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_location.mod_name: - continue - self.assertIn(board_location.name, locations_in_pool) + for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_location.mod_name: + continue + self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_then_all_orders_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories} - multi_world = setup_solo_multiworld(world_options) + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false} + with solo_multiworld(world_options) as (multi_world, _): - locations_in_pool = {location.name for location in multi_world.get_locations()} - for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: - if qi_location.mod_name: - continue - self.assertIn(qi_location.name, locations_in_pool) + locations_in_pool = {location.name for location in multi_world.get_locations()} + for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: + if qi_location.mod_name: + continue + self.assertIn(qi_location.name, locations_in_pool) - for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_location.mod_name: - continue - self.assertIn(board_location.name, locations_in_pool) + for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_location.mod_name: + continue + self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_without_arcade_machines_then_lets_play_a_game_not_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - locations_in_pool = {location.name for location in multi_world.get_locations()} - self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool) + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false} + with solo_multiworld(world_options) as (multi_world, _): + locations_in_pool = {location.name for location in multi_world.get_locations()} + self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool) diff --git a/worlds/stardew_valley/test/TestOptionsPairs.py b/worlds/stardew_valley/test/TestOptionsPairs.py index 9109c39562ee..d489ab1ff282 100644 --- a/worlds/stardew_valley/test/TestOptionsPairs.py +++ b/worlds/stardew_valley/test/TestOptionsPairs.py @@ -1,13 +1,12 @@ from . import SVTestBase from .assertion import WorldAssertMixin from .. import options -from ..options import Goal, QuestLocations class TestCrypticNoteNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_cryptic_note, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_cryptic_note, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -16,8 +15,8 @@ def test_given_option_pair_then_basic_checks(self): class TestCompleteCollectionNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_complete_collection, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_complete_collection, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -26,8 +25,8 @@ def test_given_option_pair_then_basic_checks(self): class TestProtectorOfTheValleyNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_protector_of_the_valley, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_protector_of_the_valley, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -36,8 +35,8 @@ def test_given_option_pair_then_basic_checks(self): class TestCraftMasterNoQuests(WorldAssertMixin, SVTestBase): options = { - Goal.internal_name: Goal.option_craft_master, - QuestLocations.internal_name: "none" + options.Goal.internal_name: options.Goal.option_craft_master, + options.QuestLocations.internal_name: "none" } def test_given_option_pair_then_basic_checks(self): @@ -46,8 +45,8 @@ def test_given_option_pair_then_basic_checks(self): class TestCraftMasterNoSpecialOrder(WorldAssertMixin, SVTestBase): options = { - options.Goal.internal_name: Goal.option_craft_master, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, + options.Goal.internal_name: options.Goal.option_craft_master, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.alias_disabled, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, options.Craftsanity.internal_name: options.Craftsanity.option_none } diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 0137bab9148b..bd1b67297473 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -3,8 +3,10 @@ from typing import Set from BaseClasses import get_seed -from . import SVTestCase, complete_options_with_default -from ..options import EntranceRandomization, ExcludeGingerIsland +from . import SVTestCase +from .options.utils import fill_dataclass_with_default +from .. import create_content +from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions from ..strings.entrance_names import Entrance as EntranceName from ..strings.region_names import Region as RegionName @@ -56,16 +58,19 @@ class TestEntranceRando(SVTestCase): def test_entrance_randomization(self): for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ EntranceRandomization.internal_name: option, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) + content = create_content(sv_options) seed = get_seed() rand = random.Random(seed) with self.subTest(flag=flag, msg=f"Seed: {seed}"): entrances, regions = create_final_connections_and_regions(sv_options) - _, randomized_connections = randomize_connections(rand, sv_options, regions, entrances) + _, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances) for connection in vanilla_connections: if flag in connection.flag: @@ -80,17 +85,20 @@ def test_entrance_randomization(self): def test_entrance_randomization_without_island(self): for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ EntranceRandomization.internal_name: option, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) + content = create_content(sv_options) seed = get_seed() rand = random.Random(seed) with self.subTest(option=option, flag=flag, seed=seed): entrances, regions = create_final_connections_and_regions(sv_options) - _, randomized_connections = randomize_connections(rand, sv_options, regions, entrances) + _, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances) for connection in vanilla_connections: if flag in connection.flag: @@ -109,17 +117,19 @@ def test_entrance_randomization_without_island(self): f"Connections are duplicated in randomization.") def test_cannot_put_island_access_on_island(self): - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) + content = create_content(sv_options) for i in range(0, 100 if self.skip_long_tests else 10000): seed = get_seed() rand = random.Random(seed) with self.subTest(msg=f"Seed: {seed}"): entrances, regions = create_final_connections_and_regions(sv_options) - randomized_connections, randomized_data = randomize_connections(rand, sv_options, regions, entrances) + randomized_connections, randomized_data = randomize_connections(rand, sv_options, content, regions, entrances) connections_by_name = {connection.name: connection for connection in randomized_connections} blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island} diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py deleted file mode 100644 index 3ee921bd2bc2..000000000000 --- a/worlds/stardew_valley/test/TestRules.py +++ /dev/null @@ -1,797 +0,0 @@ -from collections import Counter - -from . import SVTestBase -from .. import options, HasProgressionPercent -from ..data.craftable_data import all_crafting_recipes_by_name -from ..locations import locations_by_tag, LocationTags, location_table -from ..options import ToolProgression, BuildingProgression, ExcludeGingerIsland, Chefsanity, Craftsanity, Shipsanity, SeasonRandomization, Friendsanity, \ - FriendsanityHeartSize, BundleRandomization, SkillProgression -from ..strings.entrance_names import Entrance -from ..strings.region_names import Region -from ..strings.tool_names import Tool, ToolMaterial - - -class TestProgressiveToolsLogic(SVTestBase): - options = { - ToolProgression.internal_name: ToolProgression.option_progressive, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - } - - def test_sturgeon(self): - self.multiworld.state.prog_items = {1: Counter()} - - sturgeon_rule = self.world.logic.has("Sturgeon") - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - summer = self.world.create_item("Summer") - self.multiworld.state.collect(summer, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - fishing_rod = self.world.create_item("Progressive Fishing Rod") - self.multiworld.state.collect(fishing_rod, event=False) - self.multiworld.state.collect(fishing_rod, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - fishing_level = self.world.create_item("Fishing Level") - self.multiworld.state.collect(fishing_level, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.assert_rule_true(sturgeon_rule, self.multiworld.state) - - self.remove(summer) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - winter = self.world.create_item("Winter") - self.multiworld.state.collect(winter, event=False) - self.assert_rule_true(sturgeon_rule, self.multiworld.state) - - self.remove(fishing_rod) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - def test_old_master_cannoli(self): - self.multiworld.state.prog_items = {1: Counter()} - - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - - rule = self.world.logic.region.can_reach_location("Old Master Cannoli") - self.assert_rule_false(rule, self.multiworld.state) - - fall = self.world.create_item("Fall") - self.multiworld.state.collect(fall, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - tuesday = self.world.create_item("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - rare_seed = self.world.create_item("Rare Seed") - self.multiworld.state.collect(rare_seed, event=False) - self.assert_rule_true(rule, self.multiworld.state) - - self.remove(fall) - self.assert_rule_false(rule, self.multiworld.state) - self.remove(tuesday) - - green_house = self.world.create_item("Greenhouse") - self.multiworld.state.collect(green_house, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - friday = self.world.create_item("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, event=False) - self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) - - self.remove(green_house) - self.assert_rule_false(rule, self.multiworld.state) - self.remove(friday) - - -class TestBundlesLogic(SVTestBase): - options = { - BundleRandomization.internal_name: BundleRandomization.option_vanilla - } - - def test_vault_2500g_bundle(self): - self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) - - self.collect_lots_of_money() - self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) - - -class TestBuildingLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive - } - - def test_coop_blueprint(self): - self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) - - self.collect_lots_of_money() - self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) - - def test_big_coop_blueprint(self): - big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint") - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.collect_lots_of_money() - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=False) - self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - def test_deluxe_coop_blueprint(self): - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.collect_lots_of_money() - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) - self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - def test_big_shed_blueprint(self): - big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint") - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.collect_lots_of_money() - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Progressive Shed"), event=True) - self.assertTrue(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - -class TestArcadeMachinesLogic(SVTestBase): - options = { - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - } - - def test_prairie_king(self): - self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - - boots = self.world.create_item("JotPK: Progressive Boots") - gun = self.world.create_item("JotPK: Progressive Gun") - ammo = self.world.create_item("JotPK: Progressive Ammo") - life = self.world.create_item("JotPK: Extra Life") - drop = self.world.create_item("JotPK: Increased Drop Rate") - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(boots) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - self.remove(ammo) - self.remove(life) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) - - -class TestWeaponsLogic(SVTestBase): - options = { - ToolProgression.internal_name: ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - } - - def test_mine(self): - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.collect([self.world.create_item("Combat Level")] * 10) - self.collect([self.world.create_item("Mining Level")] * 10) - self.collect([self.world.create_item("Progressive Mine Elevator")] * 24) - self.multiworld.state.collect(self.world.create_item("Bus Repair"), event=True) - self.multiworld.state.collect(self.world.create_item("Skull Key"), event=True) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 1) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) - self.GiveItemAndCheckReachableMine("Progressive Club", 1) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 2) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 2) - self.GiveItemAndCheckReachableMine("Progressive Club", 2) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 3) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 3) - self.GiveItemAndCheckReachableMine("Progressive Club", 3) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 4) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 4) - self.GiveItemAndCheckReachableMine("Progressive Club", 4) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 5) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 5) - self.GiveItemAndCheckReachableMine("Progressive Club", 5) - - def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): - item = self.multiworld.create_item(item_name, self.player) - self.multiworld.state.collect(item, event=True) - rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40() - if reachable_level > 0: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_mines_floor_41_80() - if reachable_level > 1: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_mines_floor_81_120() - if reachable_level > 2: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_skull_cavern() - if reachable_level > 3: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.ability.can_mine_perfectly_in_the_skull_cavern() - if reachable_level > 4: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - -class TestRecipeLearnLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_learn_qos_recipe(self): - location = "Cook Radish Salad" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - self.multiworld.state.collect(self.world.create_item("Radish Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Spring"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("The Queen of Sauce"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestRecipeReceiveLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_learn_qos_recipe(self): - location = "Cook Radish Salad" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - self.multiworld.state.collect(self.world.create_item("Radish Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - spring = self.world.create_item("Spring") - qos = self.world.create_item("The Queen of Sauce") - self.multiworld.state.collect(spring, event=False) - self.multiworld.state.collect(qos, event=False) - self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.remove(spring) - self.multiworld.state.remove(qos) - - self.multiworld.state.collect(self.world.create_item("Radish Salad Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - def test_get_chefsanity_check_recipe(self): - location = "Radish Salad Recipe" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Spring"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - seeds = self.world.create_item("Radish Seeds") - summer = self.world.create_item("Summer") - house = self.world.create_item("Progressive House") - self.multiworld.state.collect(seeds, event=False) - self.multiworld.state.collect(summer, event=False) - self.multiworld.state.collect(house, event=False) - self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.remove(seeds) - self.multiworld.state.remove(summer) - self.multiworld.state.remove(house) - - self.multiworld.state.collect(self.world.create_item("The Queen of Sauce"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestCraftsanityLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - Craftsanity.internal_name: Craftsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_recipe(self): - location = "Craft Marble Brazier" - rule = self.world.logic.region.can_reach_location(location) - self.collect([self.world.create_item("Progressive Pickaxe")] * 4) - self.collect([self.world.create_item("Progressive Fishing Rod")] * 4) - self.collect([self.world.create_item("Progressive Sword")] * 4) - self.collect([self.world.create_item("Progressive Mine Elevator")] * 24) - self.collect([self.world.create_item("Mining Level")] * 10) - self.collect([self.world.create_item("Combat Level")] * 10) - self.collect([self.world.create_item("Fishing Level")] * 10) - self.collect_all_the_money() - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Marble Brazier Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_learn_crafting_recipe(self): - location = "Marble Brazier Recipe" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.collect_lots_of_money() - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Torch Recipe"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestCraftsanityWithFestivalsLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, - Craftsanity.internal_name: Craftsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Torch Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestNoCraftsanityLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - SeasonRandomization.internal_name: SeasonRandomization.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - Craftsanity.internal_name: Craftsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_recipe(self): - recipe = all_crafting_recipes_by_name["Wood Floor"] - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - result = rule(self.multiworld.state) - self.assertFalse(result) - - self.collect([self.world.create_item("Progressive Season")] * 2) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestNoCraftsanityWithFestivalsLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, - Craftsanity.internal_name: Craftsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestDonationLogicAll(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_all - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - - for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -class TestDonationLogicRandomized(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_randomized - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - donation_locations = [location for location in self.get_real_locations() if - LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] - - for donation in donation_locations: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in donation_locations: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -class TestDonationLogicMilestones(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_milestones - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - - for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -def swap_museum_and_bathhouse(multiworld, player): - museum_region = multiworld.get_region(Region.museum, player) - bathhouse_region = multiworld.get_region(Region.bathhouse_entrance, player) - museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player) - bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) - museum_entrance.connect(bathhouse_region) - bathhouse_entrance.connect(museum_region) - - -class TestToolVanillaRequiresBlacksmith(SVTestBase): - options = { - options.EntranceRandomization: options.EntranceRandomization.option_buildings, - options.ToolProgression: options.ToolProgression.option_vanilla, - } - seed = 4111845104987680262 - - # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. - - def test_cannot_get_any_tool_without_blacksmith_access(self): - railroad_item = "Railroad Boulder Removed" - place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) - collect_all_except(self.multiworld, railroad_item) - - for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: - for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: - self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: - for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: - self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) - - def test_cannot_get_fishing_rod_without_willy_access(self): - railroad_item = "Railroad Boulder Removed" - place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) - collect_all_except(self.multiworld, railroad_item) - - for fishing_rod_level in [3, 4]: - self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for fishing_rod_level in [3, 4]: - self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) - - -def place_region_at_entrance(multiworld, player, region, entrance): - region_to_place = multiworld.get_region(region, player) - entrance_to_place_region = multiworld.get_entrance(entrance, player) - - entrance_to_switch = region_to_place.entrances[0] - region_to_switch = entrance_to_place_region.connected_region - entrance_to_switch.connect(region_to_switch) - entrance_to_place_region.connect(region_to_place) - - -def collect_all_except(multiworld, item_to_not_collect: str): - for item in multiworld.get_items(): - if item.name != item_to_not_collect: - multiworld.state.collect(item) - - -class TestFriendsanityDatingRules(SVTestBase): - options = { - SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 3 - } - - def test_earning_dating_heart_requires_dating(self): - self.collect_all_the_money() - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.multiworld.state.collect(self.world.create_item("Beach Bridge"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - for i in range(3): - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Weapon"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Barn"), event=False) - for i in range(10): - self.multiworld.state.collect(self.world.create_item("Foraging Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Farming Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Mining Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Combat Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) - - npc = "Abigail" - heart_name = f"{npc} <3" - step = 3 - - self.assert_can_reach_heart_up_to(npc, 3, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 6, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 8, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 10, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 14, step) - - def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): - prefix = "Friendsanity: " - suffix = " <3" - for i in range(1, max_reachable + 1): - if i % step != 0 and i != 14: - continue - location = f"{prefix}{npc} {i}{suffix}" - can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) - self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts") - for i in range(max_reachable + 1, 14 + 1): - if i % step != 0 and i != 14: - continue - location = f"{prefix}{npc} {i}{suffix}" - can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) - self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts") - - -class TestShipsanityNone(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_none - } - - def test_no_shipsanity_locations(self): - for location in self.get_real_locations(): - self.assertFalse("Shipsanity" in location.name) - self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) - - -class TestShipsanityCrops(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_crops - } - - def test_only_crop_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) - - -class TestShipsanityFish(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_fish - } - - def test_only_fish_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) - - -class TestShipsanityFullShipment(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_full_shipment - } - - def test_only_full_shipment_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) - self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) - - -class TestShipsanityFullShipmentWithFish(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish - } - - def test_only_full_shipment_and_fish_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or - LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) - - -class TestShipsanityEverything(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_everything, - BuildingProgression.internal_name: BuildingProgression.option_progressive - } - - def test_all_shipsanity_locations_require_shipping_bin(self): - bin_name = "Shipping Bin" - collect_all_except(self.multiworld, bin_name) - shipsanity_locations = [location for location in self.get_real_locations() if - LocationTags.SHIPSANITY in location_table[location.name].tags] - bin_item = self.world.create_item(bin_name) - for location in shipsanity_locations: - with self.subTest(location.name): - self.remove(bin_item) - self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) - self.multiworld.state.collect(bin_item, event=False) - shipsanity_rule = self.world.logic.region.can_reach_location(location.name) - self.assert_rule_true(shipsanity_rule, self.multiworld.state) - self.remove(bin_item) - - -class TestVanillaSkillLogicSimplification(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)) diff --git a/worlds/stardew_valley/test/TestStardewRule.py b/worlds/stardew_valley/test/TestStardewRule.py index 89317d90e4e2..93b32b0d8ab4 100644 --- a/worlds/stardew_valley/test/TestStardewRule.py +++ b/worlds/stardew_valley/test/TestStardewRule.py @@ -1,7 +1,9 @@ import unittest +from typing import cast from unittest.mock import MagicMock, Mock -from ..stardew_rule import Received, And, Or, HasProgressionPercent, false_, true_ +from .. import StardewRule +from ..stardew_rule import Received, And, Or, HasProgressionPercent, false_, true_, Count class TestSimplification(unittest.TestCase): @@ -72,7 +74,7 @@ def test_propagate_evaluate_while_simplifying(self): collection_state = MagicMock() other_rule = MagicMock() other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, expected_result)) - rule = And(Or(other_rule)) + rule = And(Or(cast(StardewRule, other_rule))) _, actual_result = rule.evaluate_while_simplifying(collection_state) @@ -101,8 +103,9 @@ def test_short_circuit_when_complement_found(self): def test_short_circuit_when_combinable_rules_is_false(self): collection_state = MagicMock() + collection_state.has = Mock(return_value=False) other_rule = MagicMock() - rule = And(HasProgressionPercent(1, 10), other_rule) + rule = And(Received("Potato", 1, 10), cast(StardewRule, other_rule)) rule.evaluate_while_simplifying(collection_state) @@ -110,16 +113,16 @@ def test_short_circuit_when_combinable_rules_is_false(self): def test_identity_is_removed_from_other_rules(self): collection_state = MagicMock() - rule = Or(false_, HasProgressionPercent(1, 10)) + rule = Or(false_, Received("Potato", 1, 10)) rule.evaluate_while_simplifying(collection_state) self.assertEqual(1, len(rule.current_rules)) - self.assertIn(HasProgressionPercent(1, 10), rule.current_rules) + self.assertIn(Received("Potato", 1, 10), rule.current_rules) def test_complement_replaces_combinable_rules(self): collection_state = MagicMock() - rule = Or(HasProgressionPercent(1, 10), true_) + rule = Or(Received("Potato", 1, 10), true_) rule.evaluate_while_simplifying(collection_state) @@ -129,7 +132,7 @@ def test_simplifying_to_complement_propagates_complement(self): expected_simplified = true_ expected_result = True collection_state = MagicMock() - rule = Or(Or(expected_simplified), HasProgressionPercent(1, 10)) + rule = Or(Or(expected_simplified), Received("Potato", 1, 10)) actual_simplified, actual_result = rule.evaluate_while_simplifying(collection_state) @@ -141,7 +144,7 @@ def test_already_simplified_rules_are_not_simplified_again(self): collection_state = MagicMock() other_rule = MagicMock() other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, False)) - rule = Or(other_rule, HasProgressionPercent(1, 10)) + rule = Or(cast(StardewRule, other_rule), Received("Potato", 1, 10)) rule.evaluate_while_simplifying(collection_state) other_rule.assert_not_called() @@ -157,7 +160,7 @@ def test_continue_simplification_after_short_circuited(self): a_rule.evaluate_while_simplifying = Mock(return_value=(a_rule, False)) another_rule = MagicMock() another_rule.evaluate_while_simplifying = Mock(return_value=(another_rule, False)) - rule = And(a_rule, another_rule) + rule = And(cast(StardewRule, a_rule), cast(StardewRule, another_rule)) rule.evaluate_while_simplifying(collection_state) # This test is completely messed up because sets are used internally and order of the rules cannot be ensured. @@ -183,7 +186,7 @@ class TestEvaluateWhileSimplifyingDoubleCalls(unittest.TestCase): def test_nested_call_in_the_internal_rule_being_evaluated_does_check_the_internal_rule(self): collection_state = MagicMock() internal_rule = MagicMock() - rule = Or(internal_rule) + rule = Or(cast(StardewRule, internal_rule)) called_once = False internal_call_result = None @@ -212,7 +215,7 @@ def test_nested_call_to_already_simplified_rule_does_not_steal_rule_to_simplify_ an_internal_rule.evaluate_while_simplifying = Mock(return_value=(an_internal_rule, True)) another_internal_rule = MagicMock() another_internal_rule.evaluate_while_simplifying = Mock(return_value=(another_internal_rule, True)) - rule = Or(an_internal_rule, another_internal_rule) + rule = Or(cast(StardewRule, an_internal_rule), cast(StardewRule, another_internal_rule)) rule.evaluate_while_simplifying(collection_state) # This test is completely messed up because sets are used internally and order of the rules cannot be ensured. @@ -242,3 +245,61 @@ def call_to_already_simplified(state): self.assertTrue(called_once) self.assertTrue(internal_call_result) self.assertTrue(actual_result) + + +class TestCount(unittest.TestCase): + + def test_duplicate_rule_count_double(self): + expected_result = True + collection_state = MagicMock() + simplified_rule = Mock() + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule), other_rule, other_rule], 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + self.assertEqual(expected_result, actual_result) + + def test_simplified_rule_is_reused(self): + expected_result = False + collection_state = MagicMock() + simplified_rule = Mock(return_value=expected_result) + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule), cast(StardewRule, other_rule), cast(StardewRule, other_rule)], 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + self.assertEqual(expected_result, actual_result) + + other_rule.evaluate_while_simplifying.reset_mock() + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_not_called() + simplified_rule.assert_called() + self.assertEqual(expected_result, actual_result) + + def test_break_if_not_enough_rule_to_complete(self): + expected_result = False + collection_state = MagicMock() + simplified_rule = Mock() + never_called_rule = Mock() + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule)] * 4, 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + never_called_rule.assert_not_called() + never_called_rule.evaluate_while_simplifying.assert_not_called() + self.assertEqual(expected_result, actual_result) + + def test_evaluate_without_shortcircuit_when_rules_are_all_different(self): + rule = Count([cast(StardewRule, Mock()) for i in range(5)], 2) + + self.assertEqual(rule.evaluate, rule.evaluate_without_shortcircuit) diff --git a/worlds/stardew_valley/test/TestStartInventory.py b/worlds/stardew_valley/test/TestStartInventory.py index 826f49b1ac83..dc44a1bb4598 100644 --- a/worlds/stardew_valley/test/TestStartInventory.py +++ b/worlds/stardew_valley/test/TestStartInventory.py @@ -17,7 +17,7 @@ class TestStartInventoryAllsanity(WorldAssertMixin, SVTestBase): options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap, options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_only, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board, options.QuestLocations.internal_name: -1, options.Fishsanity.internal_name: options.Fishsanity.option_only_easy_fish, options.Museumsanity.internal_name: options.Museumsanity.option_randomized, @@ -29,13 +29,13 @@ class TestStartInventoryAllsanity(WorldAssertMixin, SVTestBase): options.Friendsanity.internal_name: options.Friendsanity.option_bachelors, options.FriendsanityHeartSize.internal_name: 3, options.NumberOfMovementBuffs.internal_name: 10, - options.NumberOfLuckBuffs.internal_name: 12, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, options.Mods.internal_name: ["Tractor Mod", "Bigger Backpack", "Luck Skill", "Magic", "Socializing Skill", "Archaeology", "Cooking Skill", "Binning Skill"], - "start_inventory": {"Movement Speed Bonus": 2} + "start_inventory": {"Progressive Pickaxe": 2} } - def test_start_inventory_movement_speed(self): + def test_start_inventory_progression_items_does_not_break_progression_percent(self): self.assert_basic_checks_with_subtests(self.multiworld) self.assert_can_win(self.multiworld) diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py new file mode 100644 index 000000000000..c1e8c2c8f095 --- /dev/null +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -0,0 +1,209 @@ +from . import SVTestBase +from ..options import ExcludeGingerIsland, Walnutsanity +from ..strings.ap_names.ap_option_names import WalnutsanityOptionName + + +class TestWalnutsanityNone(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: Walnutsanity.preset_none, + } + + def test_no_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 0, and collect 40 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + self.collect("Progressive House") + items = self.collect("5 Golden Walnuts", 10) + + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Island North Turtle") + self.collect("Island Resort") + self.collect("Open Professor Snail Cave") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Dig Site Bridge") + self.collect("Island Farmhouse") + self.collect("Qi Walnut Room") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Combat Level", 10) + self.collect("Mining Level", 10) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Progressive Slingshot") + self.collect("Progressive Weapon", 5) + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Watering Can", 4) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + + +class TestWalnutsanityPuzzles(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({WalnutsanityOptionName.puzzles}), + } + + def test_only_puzzle_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + def test_field_office_locations_require_professor_snail(self): + location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection", + "Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ] + locations = [location for location in self.multiworld.get_locations() if location.name in location_names] + self.collect("Island Obelisk") + self.collect("Island North Turtle") + self.collect("Island West Turtle") + self.collect("Island Resort") + self.collect("Dig Site Bridge") + self.collect("Progressive House") + self.collect("Progressive Pan") + self.collect("Progressive Fishing Rod") + self.collect("Progressive Watering Can") + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Sword", 5) + self.collect("Combat Level", 10) + self.collect("Mining Level", 10) + for location in locations: + self.assert_reach_location_false(location, self.multiworld.state) + self.collect("Open Professor Snail Cave") + for location in locations: + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestWalnutsanityBushes(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({WalnutsanityOptionName.bushes}), + } + + def test_only_bush_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityPuzzlesAndBushes(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({WalnutsanityOptionName.puzzles, WalnutsanityOptionName.bushes}), + } + + def test_only_bush_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 25, and collect 15 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + items = self.collect("5 Golden Walnuts", 5) + + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("Island North Turtle") + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + + +class TestWalnutsanityDigSpots(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({WalnutsanityOptionName.dig_spots}), + } + + def test_only_dig_spots_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertIn("Journal Scrap #6", location_names) + self.assertIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityRepeatables(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({WalnutsanityOptionName.repeatables}), + } + + def test_only_repeatable_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityAll(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: Walnutsanity.preset_all, + } + + def test_all_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertIn("Fishing Walnut 4", location_names) + self.assertIn("Journal Scrap #6", location_names) + self.assertIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 40, and collect 4 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("5 Golden Walnuts", 8) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("3 Golden Walnuts", 14) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("Golden Walnut", 40) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("5 Golden Walnuts", 4) + items = self.collect("3 Golden Walnuts", 6) + items = self.collect("Golden Walnut", 2) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 1a463d9fc280..de0ed97882e3 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,201 +1,182 @@ +import logging import os +import threading import unittest -from argparse import Namespace from contextlib import contextmanager -from typing import Dict, ClassVar, Iterable, Hashable, Tuple, Optional, List, Union, Any +from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any -from BaseClasses import MultiWorld, CollectionState, get_seed, Location -from Options import VerifyKeys -from Utils import cache_argsless +from BaseClasses import MultiWorld, CollectionState, get_seed, Location, Item from test.bases import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from worlds.AutoWorld import call_all from .assertion import RuleAssertMixin -from .. import StardewValleyWorld, options -from ..mods.mod_data import all_mods -from ..options import StardewValleyOptions, StardewValleyOption +from .options.utils import fill_namespace_with_default, parse_class_option_keys, fill_dataclass_with_default +from .. import StardewValleyWorld, options, StardewItem +from ..options import StardewValleyOption + +logger = logging.getLogger(__name__) DEFAULT_TEST_SEED = get_seed() +logger.info(f"Default Test Seed: {DEFAULT_TEST_SEED}") -# TODO is this caching really changing anything? -@cache_argsless -def disable_5_x_x_options(): +def default_6_x_x(): return { - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, - options.Chefsanity.internal_name: options.Chefsanity.option_none, - options.Craftsanity.internal_name: options.Craftsanity.option_none + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.default, + options.BackpackProgression.internal_name: options.BackpackProgression.default, + options.Booksanity.internal_name: options.Booksanity.default, + options.BuildingProgression.internal_name: options.BuildingProgression.default, + options.BundlePrice.internal_name: options.BundlePrice.default, + options.BundleRandomization.internal_name: options.BundleRandomization.default, + options.Chefsanity.internal_name: options.Chefsanity.default, + options.Cooksanity.internal_name: options.Cooksanity.default, + options.Craftsanity.internal_name: options.Craftsanity.default, + options.Cropsanity.internal_name: options.Cropsanity.default, + options.ElevatorProgression.internal_name: options.ElevatorProgression.default, + options.EntranceRandomization.internal_name: options.EntranceRandomization.default, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default, + options.FestivalLocations.internal_name: options.FestivalLocations.default, + options.Fishsanity.internal_name: options.Fishsanity.default, + options.Friendsanity.internal_name: options.Friendsanity.default, + options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default, + options.Goal.internal_name: options.Goal.default, + options.Mods.internal_name: options.Mods.default, + options.Monstersanity.internal_name: options.Monstersanity.default, + options.Museumsanity.internal_name: options.Museumsanity.default, + options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default, + options.QuestLocations.internal_name: options.QuestLocations.default, + options.SeasonRandomization.internal_name: options.SeasonRandomization.default, + options.Shipsanity.internal_name: options.Shipsanity.default, + options.SkillProgression.internal_name: options.SkillProgression.default, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.default, + options.ToolProgression.internal_name: options.ToolProgression.default, + options.TrapItems.internal_name: options.TrapItems.default, + options.Walnutsanity.internal_name: options.Walnutsanity.default } -@cache_argsless -def default_4_x_x_options(): - option_dict = default_options().copy() - option_dict.update(disable_5_x_x_options()) - option_dict.update({ - options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, - }) - return option_dict +def allsanity_no_mods_6_x_x(): + return { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, + options.Chefsanity.internal_name: options.Chefsanity.option_all, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + options.Craftsanity.internal_name: options.Craftsanity.option_all, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + options.FriendsanityHeartSize.internal_name: 1, + options.Goal.internal_name: options.Goal.option_perfection, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.NumberOfMovementBuffs.internal_name: 12, + 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_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, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all + } -@cache_argsless -def default_options(): - return {} +def allsanity_mods_6_x_x(): + allsanity = allsanity_no_mods_6_x_x() + allsanity.update({options.Mods.internal_name: frozenset(options.Mods.valid_keys)}) + return allsanity -@cache_argsless def get_minsanity_options(): return { - options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, - options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla, - options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, - options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, - options.QuestLocations.internal_name: -1, - options.Fishsanity.internal_name: options.Fishsanity.option_none, - options.Museumsanity.internal_name: options.Museumsanity.option_none, - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap, + options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla, options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Friendsanity.internal_name: options.Friendsanity.option_none, options.FriendsanityHeartSize.internal_name: 8, + options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none, options.NumberOfMovementBuffs.internal_name: 0, - options.NumberOfLuckBuffs.internal_name: 0, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.QuestLocations.internal_name: -1, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, options.TrapItems.internal_name: options.TrapItems.option_no_traps, - options.Mods.internal_name: frozenset(), + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none } -@cache_argsless def minimal_locations_maximal_items(): min_max_options = { - options.Goal.internal_name: options.Goal.option_craft_master, - options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, - options.QuestLocations.internal_name: -1, - options.Fishsanity.internal_name: options.Fishsanity.option_none, - options.Museumsanity.internal_name: options.Museumsanity.option_none, - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Friendsanity.internal_name: options.Friendsanity.option_none, options.FriendsanityHeartSize.internal_name: 8, + options.Goal.internal_name: options.Goal.option_craft_master, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.QuestLocations.internal_name: -1, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, options.TrapItems.internal_name: options.TrapItems.option_nightmare, - options.Mods.internal_name: (), + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none } return min_max_options -@cache_argsless def minimal_locations_maximal_items_with_island(): - min_max_options = minimal_locations_maximal_items().copy() + min_max_options = minimal_locations_maximal_items() min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false}) return min_max_options -@cache_argsless -def allsanity_4_x_x_options_without_mods(): - option_dict = { - options.Goal.internal_name: options.Goal.option_perfection, - options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, - options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.QuestLocations.internal_name: 56, - options.Fishsanity.internal_name: options.Fishsanity.option_all, - options.Museumsanity.internal_name: options.Museumsanity.option_all, - options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, - options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - options.Chefsanity.internal_name: options.Chefsanity.option_all, - options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.FriendsanityHeartSize.internal_name: 1, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.TrapItems.internal_name: options.TrapItems.option_nightmare, - } - option_dict.update(disable_5_x_x_options()) - return option_dict - - -@cache_argsless -def allsanity_options_without_mods(): - return { - options.Goal.internal_name: options.Goal.option_perfection, - options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, - options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.QuestLocations.internal_name: 56, - options.Fishsanity.internal_name: options.Fishsanity.option_all, - options.Museumsanity.internal_name: options.Museumsanity.option_all, - options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, - options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - options.Chefsanity.internal_name: options.Chefsanity.option_all, - options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.FriendsanityHeartSize.internal_name: 1, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.TrapItems.internal_name: options.TrapItems.option_nightmare, - } - - -@cache_argsless -def allsanity_options_with_mods(): - allsanity = allsanity_options_without_mods().copy() - allsanity.update({options.Mods.internal_name: all_mods}) - return allsanity - - class SVTestCase(unittest.TestCase): # Set False to not skip some 'extra' tests skip_base_tests: bool = True @@ -219,7 +200,6 @@ def solo_world_sub_test(self, msg: Optional[str] = None, *, seed=DEFAULT_TEST_SEED, world_caching=True, - dirty_state=False, **kwargs) -> Tuple[MultiWorld, StardewValleyWorld]: if msg is not None: msg += " " @@ -228,17 +208,8 @@ def solo_world_sub_test(self, msg: Optional[str] = None, msg += f"[Seed = {seed}]" with self.subTest(msg, **kwargs): - if world_caching: - multi_world = setup_solo_multiworld(world_options, seed) - if dirty_state: - original_state = multi_world.state.copy() - else: - multi_world = setup_solo_multiworld(world_options, seed, _cache={}) - - yield multi_world, multi_world.worlds[1] - - if world_caching and dirty_state: - multi_world.state = original_state + with solo_multiworld(world_options, seed=seed, world_caching=world_caching) as (multiworld, world): + yield multiworld, world class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): @@ -248,33 +219,59 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): seed = DEFAULT_TEST_SEED - options = get_minsanity_options() + @classmethod + def setUpClass(cls) -> None: + if cls is SVTestBase: + raise unittest.SkipTest("No running tests on SVTestBase import.") + + super().setUpClass() def world_setup(self, *args, **kwargs): self.options = parse_class_option_keys(self.options) - super().world_setup(seed=self.seed) + self.multiworld = setup_solo_multiworld(self.options, seed=self.seed) + self.multiworld.lock.acquire() + world = self.multiworld.worlds[self.player] + + self.original_state = self.multiworld.state.copy() + self.original_itempool = self.multiworld.itempool.copy() + self.unfilled_locations = self.multiworld.get_unfilled_locations(1) if self.constructed: - self.world = self.multiworld.worlds[self.player] # noqa + self.world = world # noqa + + def tearDown(self) -> None: + self.multiworld.state = self.original_state + self.multiworld.itempool = self.original_itempool + for location in self.unfilled_locations: + location.item = None + + self.multiworld.lock.release() @property def run_default_tests(self) -> bool: if self.skip_base_tests: return False - # world_setup is overridden, so it'd always run default tests when importing SVTestBase - is_not_stardew_test = type(self) is not SVTestBase - should_run_default_tests = is_not_stardew_test and super().run_default_tests - return should_run_default_tests + return super().run_default_tests - def collect_lots_of_money(self): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - for i in range(100): - self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False) + def collect_lots_of_money(self, percent: float = 0.25): + self.collect("Shipping Bin") + real_total_prog_items = self.world.total_progression_items + required_prog_items = int(round(real_total_prog_items * percent)) + self.collect("Stardrop", required_prog_items) def collect_all_the_money(self): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - for i in range(1000): - self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False) + self.collect_lots_of_money(0.95) + + def collect_everything(self): + non_event_items = [item for item in self.multiworld.get_items() if item.code] + for item in non_event_items: + self.multiworld.state.collect(item) + + def collect_all_except(self, item_to_not_collect: str): + non_event_items = [item for item in self.multiworld.get_items() if item.code] + for item in non_event_items: + if item.name != item_to_not_collect: + self.multiworld.state.collect(item) def get_real_locations(self) -> List[Location]: return [location for location in self.multiworld.get_locations(self.player) if location.address is not None] @@ -282,39 +279,87 @@ def get_real_locations(self) -> List[Location]: def get_real_location_names(self) -> List[str]: return [location.name for location in self.get_real_locations()] + def collect(self, item: Union[str, Item, Iterable[Item]], count: int = 1) -> Union[None, Item, List[Item]]: + assert count > 0 + + if not isinstance(item, str): + super().collect(item) + return + + if count == 1: + item = self.create_item(item) + self.multiworld.state.collect(item) + return item + + items = [] + for i in range(count): + item = self.create_item(item) + self.multiworld.state.collect(item) + items.append(item) + + return items + + def create_item(self, item: str) -> StardewItem: + return self.world.create_item(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 = {} +@contextmanager +def solo_multiworld(world_options: Optional[Dict[Union[str, StardewValleyOption], Any]] = None, + *, + seed=DEFAULT_TEST_SEED, + world_caching=True) -> Tuple[MultiWorld, StardewValleyWorld]: + if not world_caching: + multiworld = setup_solo_multiworld(world_options, seed, _cache={}) + yield multiworld, multiworld.worlds[1] + else: + multiworld = setup_solo_multiworld(world_options, seed) + multiworld.lock.acquire() + world = multiworld.worlds[1] + + original_state = multiworld.state.copy() + original_itempool = multiworld.itempool.copy() + unfilled_locations = multiworld.get_unfilled_locations(1) + + yield multiworld, world + + multiworld.state = original_state + multiworld.itempool = original_itempool + for location in unfilled_locations: + location.item = None + + multiworld.lock.release() + + # Mostly a copy of test.general.setup_solo_multiworld, I just don't want to change the core. def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOption], str]] = None, seed=DEFAULT_TEST_SEED, - _cache: Dict[Hashable, MultiWorld] = {}, # noqa + _cache: Dict[frozenset, MultiWorld] = {}, # noqa _steps=gen_steps) -> MultiWorld: test_options = parse_class_option_keys(test_options) # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds + # If the simple dict caching ends up taking too much memory, we could replace it with some kind of lru cache. should_cache = "start_inventory" not in test_options - frozen_options = frozenset({}) if should_cache: - frozen_options = frozenset(test_options.items()).union({seed}) - if frozen_options in _cache: - cached_multi_world = _cache[frozen_options] - print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}]") + frozen_options = frozenset(test_options.items()).union({("seed", seed)}) + cached_multi_world = search_world_cache(_cache, frozen_options) + if cached_multi_world: + print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}] [Cache size = {len(_cache)}]") return cached_multi_world multiworld = setup_base_solo_multiworld(StardewValleyWorld, (), seed=seed) # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test - args = Namespace() - for name, option in StardewValleyWorld.options_dataclass.type_hints.items(): - value = option.from_any(test_options.get(name, option.default)) - - if issubclass(option, VerifyKeys): - # Values should already be verified, but just in case... - option.verify_keys(value.value) - - setattr(args, name, {1: value}) + args = fill_namespace_with_default(test_options) multiworld.set_options(args) if "start_inventory" in test_options: @@ -326,36 +371,27 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp call_all(multiworld, step) if should_cache: - _cache[frozen_options] = multiworld - - return multiworld + add_to_world_cache(_cache, frozen_options, multiworld) # noqa + # Lock is needed for multi-threading tests + setattr(multiworld, "lock", threading.Lock()) -def parse_class_option_keys(test_options: dict) -> dict: - """ Now the option class is allowed as key. """ - parsed_options = {} - - if test_options: - for option, value in test_options.items(): - if hasattr(option, "internal_name"): - assert option.internal_name not in test_options, "Defined two times by class and internal_name" - parsed_options[option.internal_name] = value - else: - assert option in StardewValleyOptions.type_hints, \ - f"All keys of world_options must be a possible Stardew Valley option, {option} is not." - parsed_options[option] = value - - return parsed_options + return multiworld -def complete_options_with_default(options_to_complete=None) -> StardewValleyOptions: - if options_to_complete is None: - options_to_complete = {} +def search_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset) -> Optional[MultiWorld]: + try: + return cache[frozen_options] + except KeyError: + for cached_options, multi_world in cache.items(): + if frozen_options.issubset(cached_options): + return multi_world + return None - for name, option in StardewValleyOptions.type_hints.items(): - options_to_complete[name] = option.from_any(options_to_complete.get(name, option.default)) - return StardewValleyOptions(**options_to_complete) +def add_to_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset, multi_world: MultiWorld) -> None: + # We could complete the key with all the default options, but that does not seem to improve performances. + cache[frozen_options] = multi_world def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) -> MultiWorld: # noqa @@ -369,14 +405,7 @@ def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) - for i in range(1, len(test_options) + 1): multiworld.game[i] = StardewValleyWorld.game multiworld.player_name.update({i: f"Tester{i}"}) - args = Namespace() - for name, option in StardewValleyWorld.options_dataclass.type_hints.items(): - options = {} - for i in range(1, len(test_options) + 1): - player_options = test_options[i - 1] - value = option(player_options[name]) if name in player_options else option.from_any(option.default) - options.update({i: value}) - setattr(args, name, options) + args = fill_namespace_with_default(test_options) multiworld.set_options(args) for step in gen_steps: diff --git a/worlds/stardew_valley/test/assertion/mod_assert.py b/worlds/stardew_valley/test/assertion/mod_assert.py index eec7f805d2c5..baba9bbaf856 100644 --- a/worlds/stardew_valley/test/assertion/mod_assert.py +++ b/worlds/stardew_valley/test/assertion/mod_assert.py @@ -1,4 +1,4 @@ -from typing import Union, List +from typing import Union, Iterable from unittest import TestCase from BaseClasses import MultiWorld @@ -7,9 +7,11 @@ class ModAssertMixin(TestCase): - def assert_stray_mod_items(self, chosen_mods: Union[List[str], str], multiworld: MultiWorld): + def assert_stray_mod_items(self, chosen_mods: Union[Iterable[str], str], multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] + else: + chosen_mods = list(chosen_mods) if ModNames.jasper in chosen_mods: # Jasper is a weird case because it shares NPC w/ SVE... diff --git a/worlds/stardew_valley/test/assertion/option_assert.py b/worlds/stardew_valley/test/assertion/option_assert.py index b384858f34f4..a07831f73e3f 100644 --- a/worlds/stardew_valley/test/assertion/option_assert.py +++ b/worlds/stardew_valley/test/assertion/option_assert.py @@ -63,8 +63,12 @@ def assert_cropsanity_same_number_items_and_locations(self, multiworld: MultiWor all_item_names = set(get_all_item_names(multiworld)) all_location_names = set(get_all_location_names(multiworld)) all_cropsanity_item_names = {item_name for item_name in all_item_names if Group.CROPSANITY in item_table[item_name].groups} - all_cropsanity_location_names = {location_name for location_name in all_location_names if LocationTags.CROPSANITY in location_table[location_name].tags} - self.assertEqual(len(all_cropsanity_item_names), len(all_cropsanity_location_names)) + all_cropsanity_location_names = {location_name + for location_name in all_location_names + if LocationTags.CROPSANITY in location_table[location_name].tags + # Qi Beans do not have an item + and location_name != "Harvest Qi Fruit"} + self.assertEqual(len(all_cropsanity_item_names) + 1, len(all_cropsanity_location_names)) def assert_all_rarecrows_exist(self, multiworld: MultiWorld): all_item_names = set(get_all_item_names(multiworld)) diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index f9b12394311a..1031a18e115c 100644 --- a/worlds/stardew_valley/test/assertion/rule_assert.py +++ b/worlds/stardew_valley/test/assertion/rule_assert.py @@ -1,17 +1,58 @@ +from typing import List from unittest import TestCase -from BaseClasses import CollectionState -from .rule_explain import explain -from ...stardew_rule import StardewRule, false_, MISSING_ITEM +from BaseClasses import CollectionState, Location +from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach +from ...stardew_rule.rule_explain import explain class RuleAssertMixin(TestCase): def assert_rule_true(self, rule: StardewRule, state: CollectionState): - self.assertTrue(rule(state), explain(rule, state)) + expl = explain(rule, state) + try: + self.assertTrue(rule(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") + + def assert_rules_true(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_true(rule, state) def assert_rule_false(self, rule: StardewRule, state: CollectionState): - self.assertFalse(rule(state), explain(rule, state, expected=False)) + expl = explain(rule, state, expected=False) + try: + self.assertFalse(rule(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") + + def assert_rules_false(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_false(rule, state) def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: CollectionState): - self.assertNotIn(MISSING_ITEM, repr(rule)) - self.assertTrue(rule is false_ or rule(complete_state), explain(rule, complete_state)) + expl = explain(rule, complete_state) + try: + self.assertNotIn(MISSING_ITEM, repr(rule)) + self.assertTrue(rule is false_ or rule(complete_state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") + + def assert_reach_location_true(self, location: Location, state: CollectionState): + expl = explain(Reach(location.name, "Location", 1), state) + try: + can_reach = location.can_reach(state) + self.assertTrue(can_reach, expl) + except KeyError as e: + raise AssertionError(f"Error while checking location {location.name}: {e}" + f"\nExplanation: {expl}") + + def assert_reach_location_false(self, location: Location, state: CollectionState): + expl = explain(Reach(location.name, "Location", 1), state, expected=False) + try: + self.assertFalse(location.can_reach(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking location {location.name}: {e}" + f"\nExplanation: {expl}") diff --git a/worlds/stardew_valley/test/assertion/rule_explain.py b/worlds/stardew_valley/test/assertion/rule_explain.py deleted file mode 100644 index f9bf97603404..000000000000 --- a/worlds/stardew_valley/test/assertion/rule_explain.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from functools import cached_property, singledispatch -from typing import Iterable - -from BaseClasses import CollectionState -from worlds.generic.Rules import CollectionRule -from ...stardew_rule import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach - -max_explanation_depth = 10 - - -@dataclass -class RuleExplanation: - rule: StardewRule - state: CollectionState - expected: bool - sub_rules: Iterable[StardewRule] = field(default_factory=list) - - def summary(self, depth=0): - return " " * depth + f"{str(self.rule)} -> {self.result}" - - def __str__(self, depth=0): - if not self.sub_rules or depth >= max_explanation_depth: - return self.summary(depth) - - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) - if i.result is not self.expected else i.summary(depth + 1) - for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - - def __repr__(self, depth=0): - if not self.sub_rules or depth >= max_explanation_depth: - return self.summary(depth) - - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) - for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - - @cached_property - def result(self): - return self.rule(self.state) - - @cached_property - def explained_sub_rules(self): - return [_explain(i, self.state, self.expected) for i in self.sub_rules] - - -def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: - if isinstance(rule, StardewRule): - return _explain(rule, state, expected) - else: - return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa - - -@singledispatch -def _explain(rule: StardewRule, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected) - - -@_explain.register -def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.original_rules) - - -@_explain.register -def _(rule: Count, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.rules) - - -@_explain.register -def _(rule: Has, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]]) - - -@_explain.register -def _(rule: TotalReceived, state: CollectionState, expected=True) -> RuleExplanation: - return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items]) - - -@_explain.register -def _(rule: Reach, state: CollectionState, expected=True) -> RuleExplanation: - access_rules = None - if rule.resolution_hint == 'Location': - spot = state.multiworld.get_location(rule.spot, rule.player) - - if isinstance(spot.access_rule, StardewRule): - access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] - - elif rule.resolution_hint == 'Entrance': - spot = state.multiworld.get_entrance(rule.spot, rule.player) - - if isinstance(spot.access_rule, StardewRule): - access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] - - else: - spot = state.multiworld.get_region(rule.spot, rule.player) - access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)] - - if not access_rules: - return RuleExplanation(rule, state, expected) - - return RuleExplanation(rule, state, expected, access_rules) diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py index 1e5512682f92..97172834543c 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -33,14 +33,14 @@ def assert_item_was_necessary_for_victory(self, item: StardewItem, multiworld: M self.assert_can_reach_victory(multiworld) multiworld.state.remove(item) self.assert_cannot_reach_victory(multiworld) - multiworld.state.collect(item, event=False) + multiworld.state.collect(item, prevent_sweep=False) self.assert_can_reach_victory(multiworld) def assert_item_was_not_necessary_for_victory(self, item: StardewItem, multiworld: MultiWorld): self.assert_can_reach_victory(multiworld) multiworld.state.remove(item) self.assert_can_reach_victory(multiworld) - multiworld.state.collect(item, event=False) + multiworld.state.collect(item, prevent_sweep=False) self.assert_can_reach_victory(multiworld) def assert_can_win(self, multiworld: MultiWorld): @@ -53,7 +53,7 @@ def assert_same_number_items_locations(self, multiworld: MultiWorld): def assert_can_reach_everything(self, multiworld: MultiWorld): for location in multiworld.get_locations(): - self.assert_rule_true(location.access_rule, multiworld.state) + self.assert_reach_location_true(location, multiworld.state) def assert_basic_checks(self, multiworld: MultiWorld): self.assert_same_number_items_locations(multiworld) diff --git a/worlds/stardew_valley/test/content/TestArtisanEquipment.py b/worlds/stardew_valley/test/content/TestArtisanEquipment.py new file mode 100644 index 000000000000..32821511c44f --- /dev/null +++ b/worlds/stardew_valley/test/content/TestArtisanEquipment.py @@ -0,0 +1,54 @@ +from . import SVContentPackTestBase +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Vegetable, Fruit +from ...strings.food_names import Beverage +from ...strings.forageable_names import Forageable +from ...strings.machine_names import Machine +from ...strings.seed_names import Seed + +wine_base_fruits = [ + Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry, + Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.melon, Fruit.orange, Fruit.peach, + Fruit.pomegranate, Fruit.powdermelon, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit, Fruit.strawberry +] + +juice_base_vegetables = ( + Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower, + Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.kale, Vegetable.parsnip, Vegetable.potato, + Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.yam +) + +non_juice_base_vegetables = ( + Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat +) + + +class TestArtisanEquipment(SVContentPackTestBase): + + def test_keg_special_recipes(self): + self.assertIn(MachineSource(item=Vegetable.wheat, machine=Machine.keg), self.content.game_items[Beverage.beer].sources) + # self.assertIn(MachineSource(item=Ingredient.rice, machine=Machine.keg), self.content.game_items[Ingredient.vinegar].sources) + self.assertIn(MachineSource(item=Seed.coffee, machine=Machine.keg), self.content.game_items[Beverage.coffee].sources) + self.assertIn(MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg), self.content.game_items[ArtisanGood.green_tea].sources) + self.assertIn(MachineSource(item=ArtisanGood.honey, machine=Machine.keg), self.content.game_items[ArtisanGood.mead].sources) + self.assertIn(MachineSource(item=Vegetable.hops, machine=Machine.keg), self.content.game_items[ArtisanGood.pale_ale].sources) + + def test_fruits_can_be_made_into_wines(self): + + for fruit in wine_base_fruits: + with self.subTest(fruit): + self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(fruit)].sources) + self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + def test_vegetables_can_be_made_into_juices(self): + for vegetable in juice_base_vegetables: + with self.subTest(vegetable): + self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_juice(vegetable)].sources) + self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) + + def test_non_juice_vegetables_cannot_be_made_into_juices(self): + for vegetable in non_juice_base_vegetables: + with self.subTest(vegetable): + self.assertNotIn(ArtisanGood.specific_juice(vegetable), self.content.game_items) + self.assertNotIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) diff --git a/worlds/stardew_valley/test/content/TestGingerIsland.py b/worlds/stardew_valley/test/content/TestGingerIsland.py new file mode 100644 index 000000000000..7e7f866dfc8e --- /dev/null +++ b/worlds/stardew_valley/test/content/TestGingerIsland.py @@ -0,0 +1,55 @@ +from . import SVContentPackTestBase +from .. import SVTestBase +from ... import options +from ...content import content_packs +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Fruit, Vegetable +from ...strings.fish_names import Fish +from ...strings.machine_names import Machine +from ...strings.villager_names import NPC + + +class TestGingerIsland(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,) + + def test_leo_is_included(self): + self.assertIn(NPC.leo, self.content.villagers) + + def test_ginger_island_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.blue_discus, fish_names) + self.assertIn(Fish.lionfish, fish_names) + self.assertIn(Fish.stingray, fish_names) + + # 63 from pelican town + 3 ginger island exclusive + self.assertEqual(63 + 3, len(self.content.fishes)) + + def test_ginger_island_fruits_can_be_made_into_wines(self): + self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.banana)].sources) + self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources) + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.pineapple)].sources) + self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + def test_ginger_island_vegetables_can_be_made_into_wines(self): + taro_root_juice_sources = self.content.game_items[ArtisanGood.specific_juice(Vegetable.taro_root)].sources + self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), taro_root_juice_sources) + self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) + + +class TestWithoutGingerIslandE2E(SVTestBase): + options = { + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true + } + + def test_leo_is_not_in_the_pool(self): + for item in self.multiworld.itempool: + self.assertFalse(("Friendsanity: " + NPC.leo) in item.name) + + for location in self.multiworld.get_locations(self.player): + self.assertFalse(("Friendsanity: " + NPC.leo) in location.name) diff --git a/worlds/stardew_valley/test/content/TestPelicanTown.py b/worlds/stardew_valley/test/content/TestPelicanTown.py new file mode 100644 index 000000000000..fa70916c9d33 --- /dev/null +++ b/worlds/stardew_valley/test/content/TestPelicanTown.py @@ -0,0 +1,112 @@ +from . import SVContentPackTestBase +from ...strings.fish_names import Fish +from ...strings.villager_names import NPC + + +class TestPelicanTown(SVContentPackTestBase): + + def test_all_pelican_town_villagers_are_included(self): + self.assertIn(NPC.alex, self.content.villagers) + self.assertIn(NPC.elliott, self.content.villagers) + self.assertIn(NPC.harvey, self.content.villagers) + self.assertIn(NPC.sam, self.content.villagers) + self.assertIn(NPC.sebastian, self.content.villagers) + self.assertIn(NPC.shane, self.content.villagers) + self.assertIn(NPC.abigail, self.content.villagers) + self.assertIn(NPC.emily, self.content.villagers) + self.assertIn(NPC.haley, self.content.villagers) + self.assertIn(NPC.leah, self.content.villagers) + self.assertIn(NPC.maru, self.content.villagers) + self.assertIn(NPC.penny, self.content.villagers) + self.assertIn(NPC.caroline, self.content.villagers) + self.assertIn(NPC.clint, self.content.villagers) + self.assertIn(NPC.demetrius, self.content.villagers) + self.assertIn(NPC.dwarf, self.content.villagers) + self.assertIn(NPC.evelyn, self.content.villagers) + self.assertIn(NPC.george, self.content.villagers) + self.assertIn(NPC.gus, self.content.villagers) + self.assertIn(NPC.jas, self.content.villagers) + self.assertIn(NPC.jodi, self.content.villagers) + self.assertIn(NPC.kent, self.content.villagers) + self.assertIn(NPC.krobus, self.content.villagers) + self.assertIn(NPC.lewis, self.content.villagers) + self.assertIn(NPC.linus, self.content.villagers) + self.assertIn(NPC.marnie, self.content.villagers) + self.assertIn(NPC.pam, self.content.villagers) + self.assertIn(NPC.pierre, self.content.villagers) + self.assertIn(NPC.robin, self.content.villagers) + self.assertIn(NPC.sandy, self.content.villagers) + self.assertIn(NPC.vincent, self.content.villagers) + self.assertIn(NPC.willy, self.content.villagers) + self.assertIn(NPC.wizard, self.content.villagers) + + self.assertEqual(33, len(self.content.villagers)) + + def test_all_pelican_town_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.albacore, fish_names) + self.assertIn(Fish.anchovy, fish_names) + self.assertIn(Fish.bream, fish_names) + self.assertIn(Fish.bullhead, fish_names) + self.assertIn(Fish.carp, fish_names) + self.assertIn(Fish.catfish, fish_names) + self.assertIn(Fish.chub, fish_names) + self.assertIn(Fish.dorado, fish_names) + self.assertIn(Fish.eel, fish_names) + self.assertIn(Fish.flounder, fish_names) + self.assertIn(Fish.ghostfish, fish_names) + self.assertIn(Fish.goby, fish_names) + self.assertIn(Fish.halibut, fish_names) + self.assertIn(Fish.herring, fish_names) + self.assertIn(Fish.ice_pip, fish_names) + self.assertIn(Fish.largemouth_bass, fish_names) + self.assertIn(Fish.lava_eel, fish_names) + self.assertIn(Fish.lingcod, fish_names) + self.assertIn(Fish.midnight_carp, fish_names) + self.assertIn(Fish.octopus, fish_names) + self.assertIn(Fish.perch, fish_names) + self.assertIn(Fish.pike, fish_names) + self.assertIn(Fish.pufferfish, fish_names) + self.assertIn(Fish.rainbow_trout, fish_names) + self.assertIn(Fish.red_mullet, fish_names) + self.assertIn(Fish.red_snapper, fish_names) + self.assertIn(Fish.salmon, fish_names) + self.assertIn(Fish.sandfish, fish_names) + self.assertIn(Fish.sardine, fish_names) + self.assertIn(Fish.scorpion_carp, fish_names) + self.assertIn(Fish.sea_cucumber, fish_names) + self.assertIn(Fish.shad, fish_names) + self.assertIn(Fish.slimejack, fish_names) + self.assertIn(Fish.smallmouth_bass, fish_names) + self.assertIn(Fish.squid, fish_names) + self.assertIn(Fish.stonefish, fish_names) + self.assertIn(Fish.sturgeon, fish_names) + self.assertIn(Fish.sunfish, fish_names) + self.assertIn(Fish.super_cucumber, fish_names) + self.assertIn(Fish.tiger_trout, fish_names) + self.assertIn(Fish.tilapia, fish_names) + self.assertIn(Fish.tuna, fish_names) + self.assertIn(Fish.void_salmon, fish_names) + self.assertIn(Fish.walleye, fish_names) + self.assertIn(Fish.woodskip, fish_names) + self.assertIn(Fish.blobfish, fish_names) + self.assertIn(Fish.midnight_squid, fish_names) + self.assertIn(Fish.spook_fish, fish_names) + self.assertIn(Fish.angler, fish_names) + self.assertIn(Fish.crimsonfish, fish_names) + self.assertIn(Fish.glacierfish, fish_names) + self.assertIn(Fish.legend, fish_names) + self.assertIn(Fish.mutant_carp, fish_names) + self.assertIn(Fish.clam, fish_names) + self.assertIn(Fish.cockle, fish_names) + self.assertIn(Fish.crab, fish_names) + self.assertIn(Fish.crayfish, fish_names) + self.assertIn(Fish.lobster, fish_names) + self.assertIn(Fish.mussel, fish_names) + self.assertIn(Fish.oyster, fish_names) + self.assertIn(Fish.periwinkle, fish_names) + self.assertIn(Fish.shrimp, fish_names) + self.assertIn(Fish.snail, fish_names) + + self.assertEqual(63, len(self.content.fishes)) diff --git a/worlds/stardew_valley/test/content/TestQiBoard.py b/worlds/stardew_valley/test/content/TestQiBoard.py new file mode 100644 index 000000000000..b9d940d2c887 --- /dev/null +++ b/worlds/stardew_valley/test/content/TestQiBoard.py @@ -0,0 +1,27 @@ +from . import SVContentPackTestBase +from ...content import content_packs +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Fruit +from ...strings.fish_names import Fish +from ...strings.machine_names import Machine + + +class TestQiBoard(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack, content_packs.qi_board_content_pack) + + def test_extended_family_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.ms_angler, fish_names) + self.assertIn(Fish.son_of_crimsonfish, fish_names) + self.assertIn(Fish.glacierfish_jr, fish_names) + self.assertIn(Fish.legend_ii, fish_names) + self.assertIn(Fish.radioactive_carp, fish_names) + + # 63 from pelican town + 3 ginger island exclusive + 5 extended family + self.assertEqual(63 + 3 + 5, len(self.content.fishes)) + + def test_wines(self): + self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.qi_fruit)].sources) + self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) diff --git a/worlds/stardew_valley/test/content/__init__.py b/worlds/stardew_valley/test/content/__init__.py new file mode 100644 index 000000000000..c666a3aae14d --- /dev/null +++ b/worlds/stardew_valley/test/content/__init__.py @@ -0,0 +1,24 @@ +import unittest +from typing import ClassVar, Tuple + +from ...content import content_packs, ContentPack, StardewContent, unpack_content, StardewFeatures, feature + +default_features = StardewFeatures( + feature.booksanity.BooksanityDisabled(), + feature.cropsanity.CropsanityDisabled(), + feature.fishsanity.FishsanityNone(), + feature.friendsanity.FriendsanityNone(), + feature.skill_progression.SkillProgressionVanilla(), +) + + +class SVContentPackTestBase(unittest.TestCase): + vanilla_packs: ClassVar[Tuple[ContentPack]] = (content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines) + mods: ClassVar[Tuple[str]] = () + + content: ClassVar[StardewContent] + + @classmethod + def setUpClass(cls) -> None: + packs = cls.vanilla_packs + tuple(content_packs.by_mod[mod] for mod in cls.mods) + cls.content = unpack_content(default_features, packs) diff --git a/worlds/stardew_valley/test/content/feature/TestFriendsanity.py b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py new file mode 100644 index 000000000000..804ac0978bb5 --- /dev/null +++ b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py @@ -0,0 +1,33 @@ +import unittest + +from ....content.feature import friendsanity + + +class TestHeartSteps(unittest.TestCase): + + def test_given_size_of_one_when_calculate_steps_then_advance_one_heart_at_the_time(self): + steps = friendsanity.get_heart_steps(4, 1) + + self.assertEqual(steps, (1, 2, 3, 4)) + + def test_given_size_of_two_when_calculate_steps_then_advance_two_heart_at_the_time(self): + steps = friendsanity.get_heart_steps(6, 2) + + self.assertEqual(steps, (2, 4, 6)) + + def test_given_size_of_three_and_max_heart_not_multiple_of_three_when_calculate_steps_then_add_max_as_last_step(self): + steps = friendsanity.get_heart_steps(7, 3) + + self.assertEqual(steps, (3, 6, 7)) + + +class TestExtractNpcFromLocation(unittest.TestCase): + + def test_given_npc_with_space_in_name_when_extract_then_find_name_and_heart(self): + npc = "Mr. Ginger" + location_name = friendsanity.to_location_name(npc, 34) + + found_name, found_heart = friendsanity.extract_npc_from_location_name(location_name) + + self.assertEqual(found_name, npc) + self.assertEqual(found_heart, 34) diff --git a/worlds/stardew_valley/test/content/feature/__init__.py b/worlds/stardew_valley/test/content/feature/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/content/mods/TestDeepwoods.py b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py new file mode 100644 index 000000000000..381502da13ba --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py @@ -0,0 +1,14 @@ +from ....data.artisan import MachineSource +from ....mods.mod_data import ModNames +from ....strings.artisan_good_names import ArtisanGood +from ....strings.crop_names import Fruit +from ....strings.machine_names import Machine +from ....test.content import SVContentPackTestBase + + +class TestArtisanEquipment(SVContentPackTestBase): + mods = (ModNames.deepwoods,) + + def test_mango_wine_exists(self): + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources) + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) diff --git a/worlds/stardew_valley/test/content/mods/TestJasper.py b/worlds/stardew_valley/test/content/mods/TestJasper.py new file mode 100644 index 000000000000..40927e67c258 --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestJasper.py @@ -0,0 +1,27 @@ +from .. import SVContentPackTestBase +from ....mods.mod_data import ModNames +from ....strings.villager_names import ModNPC + + +class TestJasperWithoutSVE(SVContentPackTestBase): + mods = (ModNames.jasper,) + + def test_gunther_is_added(self): + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.jasper) + + def test_marlon_is_added(self): + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.jasper) + + +class TestJasperWithSVE(SVContentPackTestBase): + mods = (ModNames.jasper, ModNames.sve) + + def test_gunther_is_added(self): + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.sve) + + def test_marlon_is_added(self): + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.sve) diff --git a/worlds/stardew_valley/test/content/mods/TestSVE.py b/worlds/stardew_valley/test/content/mods/TestSVE.py new file mode 100644 index 000000000000..4065498d6be7 --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestSVE.py @@ -0,0 +1,143 @@ +from .. import SVContentPackTestBase +from ... import SVTestBase +from .... import options +from ....content import content_packs +from ....mods.mod_data import ModNames +from ....strings.fish_names import SVEFish +from ....strings.villager_names import ModNPC, NPC + +vanilla_villagers = 33 +vanilla_villagers_with_leo = 34 +sve_villagers = 13 +sve_villagers_with_lance = 14 +vanilla_pelican_town_fish = 63 +vanilla_ginger_island_fish = 3 +sve_pelican_town_fish = 16 +sve_ginger_island_fish = 10 + + +class TestVanilla(SVContentPackTestBase): + + def test_wizard_is_not_bachelor(self): + self.assertFalse(self.content.villagers[NPC.wizard].bachelor) + + +class TestSVE(SVContentPackTestBase): + mods = (ModNames.sve,) + + def test_lance_is_not_included(self): + self.assertNotIn(ModNPC.lance, self.content.villagers) + + def test_wizard_is_bachelor(self): + self.assertTrue(self.content.villagers[NPC.wizard].bachelor) + self.assertEqual(self.content.villagers[NPC.wizard].mod_name, ModNames.sve) + + def test_sve_npc_are_included(self): + self.assertIn(ModNPC.apples, self.content.villagers) + self.assertIn(ModNPC.claire, self.content.villagers) + self.assertIn(ModNPC.olivia, self.content.villagers) + self.assertIn(ModNPC.sophia, self.content.villagers) + self.assertIn(ModNPC.victor, self.content.villagers) + self.assertIn(ModNPC.andy, self.content.villagers) + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertIn(ModNPC.martin, self.content.villagers) + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertIn(ModNPC.morgan, self.content.villagers) + self.assertIn(ModNPC.morris, self.content.villagers) + self.assertIn(ModNPC.scarlett, self.content.villagers) + self.assertIn(ModNPC.susan, self.content.villagers) + + self.assertEqual(vanilla_villagers + sve_villagers, len(self.content.villagers)) + + def test_sve_has_sve_fish(self): + fish_names = self.content.fishes.keys() + + self.assertIn(SVEFish.bonefish, fish_names) + self.assertIn(SVEFish.bull_trout, fish_names) + self.assertIn(SVEFish.butterfish, fish_names) + self.assertIn(SVEFish.frog, fish_names) + self.assertIn(SVEFish.goldenfish, fish_names) + self.assertIn(SVEFish.grass_carp, fish_names) + self.assertIn(SVEFish.king_salmon, fish_names) + self.assertIn(SVEFish.kittyfish, fish_names) + self.assertIn(SVEFish.meteor_carp, fish_names) + self.assertIn(SVEFish.minnow, fish_names) + self.assertIn(SVEFish.puppyfish, fish_names) + self.assertIn(SVEFish.radioactive_bass, fish_names) + self.assertIn(SVEFish.snatcher_worm, fish_names) + self.assertIn(SVEFish.undeadfish, fish_names) + self.assertIn(SVEFish.void_eel, fish_names) + self.assertIn(SVEFish.water_grub, fish_names) + + self.assertEqual(vanilla_pelican_town_fish + sve_pelican_town_fish, len(self.content.fishes)) + + +class TestSVEWithGingerIsland(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,) + mods = (ModNames.sve,) + + def test_lance_is_included(self): + self.assertIn(ModNPC.lance, self.content.villagers) + + def test_other_sve_npc_are_included(self): + self.assertIn(ModNPC.apples, self.content.villagers) + self.assertIn(ModNPC.claire, self.content.villagers) + self.assertIn(ModNPC.olivia, self.content.villagers) + self.assertIn(ModNPC.sophia, self.content.villagers) + self.assertIn(ModNPC.victor, self.content.villagers) + self.assertIn(ModNPC.andy, self.content.villagers) + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertIn(ModNPC.martin, self.content.villagers) + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertIn(ModNPC.morgan, self.content.villagers) + self.assertIn(ModNPC.morris, self.content.villagers) + self.assertIn(ModNPC.scarlett, self.content.villagers) + self.assertIn(ModNPC.susan, self.content.villagers) + + self.assertEqual(vanilla_villagers_with_leo + sve_villagers_with_lance, len(self.content.villagers)) + + def test_sve_has_sve_fish(self): + fish_names = self.content.fishes.keys() + + self.assertIn(SVEFish.baby_lunaloo, fish_names) + self.assertIn(SVEFish.bonefish, fish_names) + self.assertIn(SVEFish.bull_trout, fish_names) + self.assertIn(SVEFish.butterfish, fish_names) + self.assertIn(SVEFish.clownfish, fish_names) + self.assertIn(SVEFish.daggerfish, fish_names) + self.assertIn(SVEFish.frog, fish_names) + self.assertIn(SVEFish.gemfish, fish_names) + self.assertIn(SVEFish.goldenfish, fish_names) + self.assertIn(SVEFish.grass_carp, fish_names) + self.assertIn(SVEFish.king_salmon, fish_names) + self.assertIn(SVEFish.kittyfish, fish_names) + self.assertIn(SVEFish.lunaloo, fish_names) + self.assertIn(SVEFish.meteor_carp, fish_names) + self.assertIn(SVEFish.minnow, fish_names) + self.assertIn(SVEFish.puppyfish, fish_names) + self.assertIn(SVEFish.radioactive_bass, fish_names) + self.assertIn(SVEFish.seahorse, fish_names) + self.assertIn(SVEFish.shiny_lunaloo, fish_names) + self.assertIn(SVEFish.snatcher_worm, fish_names) + self.assertIn(SVEFish.starfish, fish_names) + self.assertIn(SVEFish.torpedo_trout, fish_names) + self.assertIn(SVEFish.undeadfish, fish_names) + self.assertIn(SVEFish.void_eel, fish_names) + self.assertIn(SVEFish.water_grub, fish_names) + self.assertIn(SVEFish.sea_sponge, fish_names) + + self.assertEqual(vanilla_pelican_town_fish + vanilla_ginger_island_fish + sve_pelican_town_fish + sve_ginger_island_fish, len(self.content.fishes)) + + +class TestSVEWithoutGingerIslandE2E(SVTestBase): + options = { + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.Mods: ModNames.sve + } + + def test_lance_is_not_in_the_pool(self): + for item in self.multiworld.itempool: + self.assertFalse(("Friendsanity: " + ModNPC.lance) in item.name) + + for location in self.multiworld.get_locations(self.player): + self.assertFalse(("Friendsanity: " + ModNPC.lance) in location.name) diff --git a/worlds/stardew_valley/test/content/mods/__init__.py b/worlds/stardew_valley/test/content/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py index 9f76c10a9da4..395c48ee698a 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -2,22 +2,25 @@ from itertools import combinations, product from BaseClasses import get_seed -from .option_names import all_option_choices +from .option_names import all_option_choices, get_option_choices from .. import SVTestCase from ..assertion import WorldAssertMixin, ModAssertMixin from ... import options -from ...mods.mod_data import all_mods, ModNames +from ...mods.mod_data import ModNames assert unittest class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): - def test_given_mod_pairs_when_generate_then_basic_checks(self): - if self.skip_long_tests: - return + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + if cls.skip_long_tests: + raise unittest.SkipTest("Long tests disabled") - for mod_pair in combinations(all_mods, 2): + def test_given_mod_pairs_when_generate_then_basic_checks(self): + for mod_pair in combinations(options.Mods.valid_keys, 2): world_options = { options.Mods: frozenset(mod_pair) } @@ -27,10 +30,7 @@ def test_given_mod_pairs_when_generate_then_basic_checks(self): self.assert_stray_mod_items(list(mod_pair), multiworld) def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self): - if self.skip_long_tests: - return - - for mod, (option, value) in product(all_mods, all_option_choices): + for mod, (option, value) in product(options.Mods.valid_keys, all_option_choices): world_options = { option: value, options.Mods: mod @@ -40,12 +40,28 @@ def test_given_mod_names_when_generate_paired_with_other_options_then_basic_chec self.assert_basic_checks(multiworld) self.assert_stray_mod_items(mod, multiworld) - # @unittest.skip + def test_given_no_quest_all_mods_when_generate_with_all_goals_then_basic_checks(self): + for goal, (option, value) in product(get_option_choices(options.Goal), all_option_choices): + if option is options.QuestLocations: + continue + + world_options = { + options.Goal: goal, + option: value, + options.QuestLocations: -1, + options.Mods: frozenset(options.Mods.valid_keys), + } + + with self.solo_world_sub_test(f"Goal: {goal}, {option.internal_name}: {value}", world_options, world_caching=False) as (multiworld, _): + self.assert_basic_checks(multiworld) + + @unittest.skip def test_troubleshoot_option(self): - seed = get_seed(45949559493817417717) + seed = get_seed(78709133382876990000) + world_options = { - options.ElevatorProgression: options.ElevatorProgression.option_vanilla, - options.Mods: ModNames.deepwoods + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.Mods: ModNames.sve } with self.solo_world_sub_test(world_options=world_options, seed=seed, world_caching=False) as (multiworld, _): diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index ca9fc01b2922..0c8cfcb1e107 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -1,15 +1,18 @@ +import unittest from itertools import combinations +from BaseClasses import get_seed from .option_names import all_option_choices -from .. import setup_solo_multiworld, SVTestCase +from .. import SVTestCase, solo_multiworld from ..assertion.world_assert import WorldAssertMixin from ... import options +from ...mods.mod_data import ModNames class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") for (option1, option1_choice), (option2, option2_choice) in combinations(all_option_choices, 2): if option1 is option2: @@ -31,13 +34,14 @@ class TestDynamicOptionDebug(WorldAssertMixin, SVTestCase): def test_option_pair_debug(self): option_dict = { - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.Monstersanity.internal_name: options.Monstersanity.option_one_per_monster, + options.Goal.internal_name: options.Goal.option_master_angler, + options.QuestLocations.internal_name: -1, + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Mods.internal_name: frozenset({ModNames.sve}), } for i in range(1): - # seed = int(random() * pow(10, 18) - 1) - seed = 823942126251776128 + seed = get_seed() with self.subTest(f"Seed: {seed}"): print(f"Seed: {seed}") - multiworld = setup_solo_multiworld(option_dict, seed) - self.assert_basic_checks(multiworld) + with solo_multiworld(option_dict, seed=seed) as (multiworld, _): + self.assert_basic_checks(multiworld) diff --git a/worlds/stardew_valley/test/long/TestPreRolledRandomness.py b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py index 66bc5aeba8bb..f233fc36dc84 100644 --- a/worlds/stardew_valley/test/long/TestPreRolledRandomness.py +++ b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py @@ -1,3 +1,5 @@ +import unittest + from BaseClasses import get_seed from .. import SVTestCase from ..assertion import WorldAssertMixin @@ -7,7 +9,8 @@ class TestGeneratePreRolledRandomness(WorldAssertMixin, SVTestCase): def test_given_pre_rolled_difficult_randomness_when_generate_then_basic_checks(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") + choices = { options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings, options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index f3702c05f42b..6d4931280a79 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -1,10 +1,11 @@ import random +import unittest from typing import Dict from BaseClasses import MultiWorld, get_seed from Options import NamedRange, Range from .option_names import options_to_include -from .. import setup_solo_multiworld, SVTestCase +from .. import SVTestCase from ..assertion import GoalAssertMixin, OptionAssertMixin, WorldAssertMixin @@ -18,12 +19,6 @@ def get_option_choices(option) -> Dict[str, int]: return {} -def generate_random_multiworld(world_id: int): - world_options = generate_random_world_options(world_id) - multiworld = setup_solo_multiworld(world_options, seed=world_id) - return multiworld - - def generate_random_world_options(seed: int) -> Dict[str, int]: num_options = len(options_to_include) world_options = dict() @@ -57,7 +52,8 @@ def get_number_log_steps(number_worlds: int) -> int: class TestGenerateManyWorlds(GoalAssertMixin, OptionAssertMixin, WorldAssertMixin, SVTestCase): def test_generate_many_worlds_then_check_results(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") + number_worlds = 10 if self.skip_long_tests else 1000 seed = get_seed() self.generate_and_check_many_worlds(number_worlds, seed) diff --git a/worlds/stardew_valley/test/mods/TestModFish.py b/worlds/stardew_valley/test/mods/TestModFish.py deleted file mode 100644 index 81ac6ac0fb99..000000000000 --- a/worlds/stardew_valley/test/mods/TestModFish.py +++ /dev/null @@ -1,226 +0,0 @@ -import unittest -from typing import Set - -from ...data.fish_data import get_fish_for_mods -from ...mods.mod_data import ModNames -from ...strings.fish_names import Fish, SVEFish - -no_mods: Set[str] = set() -sve: Set[str] = {ModNames.sve} - - -class TestGetFishForMods(unittest.TestCase): - - def test_no_mods_all_vanilla_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(Fish.albacore, fish_names) - self.assertIn(Fish.anchovy, fish_names) - self.assertIn(Fish.blue_discus, fish_names) - self.assertIn(Fish.bream, fish_names) - self.assertIn(Fish.bullhead, fish_names) - self.assertIn(Fish.carp, fish_names) - self.assertIn(Fish.catfish, fish_names) - self.assertIn(Fish.chub, fish_names) - self.assertIn(Fish.dorado, fish_names) - self.assertIn(Fish.eel, fish_names) - self.assertIn(Fish.flounder, fish_names) - self.assertIn(Fish.ghostfish, fish_names) - self.assertIn(Fish.halibut, fish_names) - self.assertIn(Fish.herring, fish_names) - self.assertIn(Fish.ice_pip, fish_names) - self.assertIn(Fish.largemouth_bass, fish_names) - self.assertIn(Fish.lava_eel, fish_names) - self.assertIn(Fish.lingcod, fish_names) - self.assertIn(Fish.lionfish, fish_names) - self.assertIn(Fish.midnight_carp, fish_names) - self.assertIn(Fish.octopus, fish_names) - self.assertIn(Fish.perch, fish_names) - self.assertIn(Fish.pike, fish_names) - self.assertIn(Fish.pufferfish, fish_names) - self.assertIn(Fish.rainbow_trout, fish_names) - self.assertIn(Fish.red_mullet, fish_names) - self.assertIn(Fish.red_snapper, fish_names) - self.assertIn(Fish.salmon, fish_names) - self.assertIn(Fish.sandfish, fish_names) - self.assertIn(Fish.sardine, fish_names) - self.assertIn(Fish.scorpion_carp, fish_names) - self.assertIn(Fish.sea_cucumber, fish_names) - self.assertIn(Fish.shad, fish_names) - self.assertIn(Fish.slimejack, fish_names) - self.assertIn(Fish.smallmouth_bass, fish_names) - self.assertIn(Fish.squid, fish_names) - self.assertIn(Fish.stingray, fish_names) - self.assertIn(Fish.stonefish, fish_names) - self.assertIn(Fish.sturgeon, fish_names) - self.assertIn(Fish.sunfish, fish_names) - self.assertIn(Fish.super_cucumber, fish_names) - self.assertIn(Fish.tiger_trout, fish_names) - self.assertIn(Fish.tilapia, fish_names) - self.assertIn(Fish.tuna, fish_names) - self.assertIn(Fish.void_salmon, fish_names) - self.assertIn(Fish.walleye, fish_names) - self.assertIn(Fish.woodskip, fish_names) - self.assertIn(Fish.blob_fish, fish_names) - self.assertIn(Fish.midnight_squid, fish_names) - self.assertIn(Fish.spook_fish, fish_names) - self.assertIn(Fish.angler, fish_names) - self.assertIn(Fish.crimsonfish, fish_names) - self.assertIn(Fish.glacierfish, fish_names) - self.assertIn(Fish.legend, fish_names) - self.assertIn(Fish.mutant_carp, fish_names) - self.assertIn(Fish.ms_angler, fish_names) - self.assertIn(Fish.son_of_crimsonfish, fish_names) - self.assertIn(Fish.glacierfish_jr, fish_names) - self.assertIn(Fish.legend_ii, fish_names) - self.assertIn(Fish.radioactive_carp, fish_names) - self.assertIn(Fish.clam, fish_names) - self.assertIn(Fish.cockle, fish_names) - self.assertIn(Fish.crab, fish_names) - self.assertIn(Fish.crayfish, fish_names) - self.assertIn(Fish.lobster, fish_names) - self.assertIn(Fish.mussel, fish_names) - self.assertIn(Fish.oyster, fish_names) - self.assertIn(Fish.periwinkle, fish_names) - self.assertIn(Fish.shrimp, fish_names) - self.assertIn(Fish.snail, fish_names) - - def test_no_mods_no_sve_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertNotIn(SVEFish.baby_lunaloo, fish_names) - self.assertNotIn(SVEFish.bonefish, fish_names) - self.assertNotIn(SVEFish.bull_trout, fish_names) - self.assertNotIn(SVEFish.butterfish, fish_names) - self.assertNotIn(SVEFish.clownfish, fish_names) - self.assertNotIn(SVEFish.daggerfish, fish_names) - self.assertNotIn(SVEFish.frog, fish_names) - self.assertNotIn(SVEFish.gemfish, fish_names) - self.assertNotIn(SVEFish.goldenfish, fish_names) - self.assertNotIn(SVEFish.grass_carp, fish_names) - self.assertNotIn(SVEFish.king_salmon, fish_names) - self.assertNotIn(SVEFish.kittyfish, fish_names) - self.assertNotIn(SVEFish.lunaloo, fish_names) - self.assertNotIn(SVEFish.meteor_carp, fish_names) - self.assertNotIn(SVEFish.minnow, fish_names) - self.assertNotIn(SVEFish.puppyfish, fish_names) - self.assertNotIn(SVEFish.radioactive_bass, fish_names) - self.assertNotIn(SVEFish.seahorse, fish_names) - self.assertNotIn(SVEFish.shiny_lunaloo, fish_names) - self.assertNotIn(SVEFish.snatcher_worm, fish_names) - self.assertNotIn(SVEFish.starfish, fish_names) - self.assertNotIn(SVEFish.torpedo_trout, fish_names) - self.assertNotIn(SVEFish.undeadfish, fish_names) - self.assertNotIn(SVEFish.void_eel, fish_names) - self.assertNotIn(SVEFish.water_grub, fish_names) - self.assertNotIn(SVEFish.sea_sponge, fish_names) - self.assertNotIn(SVEFish.dulse_seaweed, fish_names) - - def test_sve_all_vanilla_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(Fish.albacore, fish_names) - self.assertIn(Fish.anchovy, fish_names) - self.assertIn(Fish.blue_discus, fish_names) - self.assertIn(Fish.bream, fish_names) - self.assertIn(Fish.bullhead, fish_names) - self.assertIn(Fish.carp, fish_names) - self.assertIn(Fish.catfish, fish_names) - self.assertIn(Fish.chub, fish_names) - self.assertIn(Fish.dorado, fish_names) - self.assertIn(Fish.eel, fish_names) - self.assertIn(Fish.flounder, fish_names) - self.assertIn(Fish.ghostfish, fish_names) - self.assertIn(Fish.halibut, fish_names) - self.assertIn(Fish.herring, fish_names) - self.assertIn(Fish.ice_pip, fish_names) - self.assertIn(Fish.largemouth_bass, fish_names) - self.assertIn(Fish.lava_eel, fish_names) - self.assertIn(Fish.lingcod, fish_names) - self.assertIn(Fish.lionfish, fish_names) - self.assertIn(Fish.midnight_carp, fish_names) - self.assertIn(Fish.octopus, fish_names) - self.assertIn(Fish.perch, fish_names) - self.assertIn(Fish.pike, fish_names) - self.assertIn(Fish.pufferfish, fish_names) - self.assertIn(Fish.rainbow_trout, fish_names) - self.assertIn(Fish.red_mullet, fish_names) - self.assertIn(Fish.red_snapper, fish_names) - self.assertIn(Fish.salmon, fish_names) - self.assertIn(Fish.sandfish, fish_names) - self.assertIn(Fish.sardine, fish_names) - self.assertIn(Fish.scorpion_carp, fish_names) - self.assertIn(Fish.sea_cucumber, fish_names) - self.assertIn(Fish.shad, fish_names) - self.assertIn(Fish.slimejack, fish_names) - self.assertIn(Fish.smallmouth_bass, fish_names) - self.assertIn(Fish.squid, fish_names) - self.assertIn(Fish.stingray, fish_names) - self.assertIn(Fish.stonefish, fish_names) - self.assertIn(Fish.sturgeon, fish_names) - self.assertIn(Fish.sunfish, fish_names) - self.assertIn(Fish.super_cucumber, fish_names) - self.assertIn(Fish.tiger_trout, fish_names) - self.assertIn(Fish.tilapia, fish_names) - self.assertIn(Fish.tuna, fish_names) - self.assertIn(Fish.void_salmon, fish_names) - self.assertIn(Fish.walleye, fish_names) - self.assertIn(Fish.woodskip, fish_names) - self.assertIn(Fish.blob_fish, fish_names) - self.assertIn(Fish.midnight_squid, fish_names) - self.assertIn(Fish.spook_fish, fish_names) - self.assertIn(Fish.angler, fish_names) - self.assertIn(Fish.crimsonfish, fish_names) - self.assertIn(Fish.glacierfish, fish_names) - self.assertIn(Fish.legend, fish_names) - self.assertIn(Fish.mutant_carp, fish_names) - self.assertIn(Fish.ms_angler, fish_names) - self.assertIn(Fish.son_of_crimsonfish, fish_names) - self.assertIn(Fish.glacierfish_jr, fish_names) - self.assertIn(Fish.legend_ii, fish_names) - self.assertIn(Fish.radioactive_carp, fish_names) - self.assertIn(Fish.clam, fish_names) - self.assertIn(Fish.cockle, fish_names) - self.assertIn(Fish.crab, fish_names) - self.assertIn(Fish.crayfish, fish_names) - self.assertIn(Fish.lobster, fish_names) - self.assertIn(Fish.mussel, fish_names) - self.assertIn(Fish.oyster, fish_names) - self.assertIn(Fish.periwinkle, fish_names) - self.assertIn(Fish.shrimp, fish_names) - self.assertIn(Fish.snail, fish_names) - - def test_sve_has_sve_fish(self): - all_fish = get_fish_for_mods(sve) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(SVEFish.baby_lunaloo, fish_names) - self.assertIn(SVEFish.bonefish, fish_names) - self.assertIn(SVEFish.bull_trout, fish_names) - self.assertIn(SVEFish.butterfish, fish_names) - self.assertIn(SVEFish.clownfish, fish_names) - self.assertIn(SVEFish.daggerfish, fish_names) - self.assertIn(SVEFish.frog, fish_names) - self.assertIn(SVEFish.gemfish, fish_names) - self.assertIn(SVEFish.goldenfish, fish_names) - self.assertIn(SVEFish.grass_carp, fish_names) - self.assertIn(SVEFish.king_salmon, fish_names) - self.assertIn(SVEFish.kittyfish, fish_names) - self.assertIn(SVEFish.lunaloo, fish_names) - self.assertIn(SVEFish.meteor_carp, fish_names) - self.assertIn(SVEFish.minnow, fish_names) - self.assertIn(SVEFish.puppyfish, fish_names) - self.assertIn(SVEFish.radioactive_bass, fish_names) - self.assertIn(SVEFish.seahorse, fish_names) - self.assertIn(SVEFish.shiny_lunaloo, fish_names) - self.assertIn(SVEFish.snatcher_worm, fish_names) - self.assertIn(SVEFish.starfish, fish_names) - self.assertIn(SVEFish.torpedo_trout, fish_names) - self.assertIn(SVEFish.undeadfish, fish_names) - self.assertIn(SVEFish.void_eel, fish_names) - self.assertIn(SVEFish.water_grub, fish_names) - self.assertIn(SVEFish.sea_sponge, fish_names) - self.assertIn(SVEFish.dulse_seaweed, fish_names) diff --git a/worlds/stardew_valley/test/mods/TestModVillagers.py b/worlds/stardew_valley/test/mods/TestModVillagers.py deleted file mode 100644 index 3be437c3f737..000000000000 --- a/worlds/stardew_valley/test/mods/TestModVillagers.py +++ /dev/null @@ -1,132 +0,0 @@ -import unittest -from typing import Set - -from ...data.villagers_data import get_villagers_for_mods -from ...mods.mod_data import ModNames -from ...strings.villager_names import NPC, ModNPC - -no_mods: Set[str] = set() -sve: Set[str] = {ModNames.sve} - - -class TestGetVillagersForMods(unittest.TestCase): - - def test_no_mods_all_vanilla_villagers(self): - villagers = get_villagers_for_mods(no_mods) - villager_names = {villager.name for villager in villagers} - - self.assertIn(NPC.alex, villager_names) - self.assertIn(NPC.elliott, villager_names) - self.assertIn(NPC.harvey, villager_names) - self.assertIn(NPC.sam, villager_names) - self.assertIn(NPC.sebastian, villager_names) - self.assertIn(NPC.shane, villager_names) - self.assertIn(NPC.abigail, villager_names) - self.assertIn(NPC.emily, villager_names) - self.assertIn(NPC.haley, villager_names) - self.assertIn(NPC.leah, villager_names) - self.assertIn(NPC.maru, villager_names) - self.assertIn(NPC.penny, villager_names) - self.assertIn(NPC.caroline, villager_names) - self.assertIn(NPC.clint, villager_names) - self.assertIn(NPC.demetrius, villager_names) - self.assertIn(NPC.dwarf, villager_names) - self.assertIn(NPC.evelyn, villager_names) - self.assertIn(NPC.george, villager_names) - self.assertIn(NPC.gus, villager_names) - self.assertIn(NPC.jas, villager_names) - self.assertIn(NPC.jodi, villager_names) - self.assertIn(NPC.kent, villager_names) - self.assertIn(NPC.krobus, villager_names) - self.assertIn(NPC.leo, villager_names) - self.assertIn(NPC.lewis, villager_names) - self.assertIn(NPC.linus, villager_names) - self.assertIn(NPC.marnie, villager_names) - self.assertIn(NPC.pam, villager_names) - self.assertIn(NPC.pierre, villager_names) - self.assertIn(NPC.robin, villager_names) - self.assertIn(NPC.sandy, villager_names) - self.assertIn(NPC.vincent, villager_names) - self.assertIn(NPC.willy, villager_names) - self.assertIn(NPC.wizard, villager_names) - - def test_no_mods_no_mod_villagers(self): - villagers = get_villagers_for_mods(no_mods) - villager_names = {villager.name for villager in villagers} - - self.assertNotIn(ModNPC.alec, villager_names) - self.assertNotIn(ModNPC.ayeisha, villager_names) - self.assertNotIn(ModNPC.delores, villager_names) - self.assertNotIn(ModNPC.eugene, villager_names) - self.assertNotIn(ModNPC.jasper, villager_names) - self.assertNotIn(ModNPC.juna, villager_names) - self.assertNotIn(ModNPC.mr_ginger, villager_names) - self.assertNotIn(ModNPC.riley, villager_names) - self.assertNotIn(ModNPC.shiko, villager_names) - self.assertNotIn(ModNPC.wellwick, villager_names) - self.assertNotIn(ModNPC.yoba, villager_names) - self.assertNotIn(ModNPC.lance, villager_names) - self.assertNotIn(ModNPC.apples, villager_names) - self.assertNotIn(ModNPC.claire, villager_names) - self.assertNotIn(ModNPC.olivia, villager_names) - self.assertNotIn(ModNPC.sophia, villager_names) - self.assertNotIn(ModNPC.victor, villager_names) - self.assertNotIn(ModNPC.andy, villager_names) - self.assertNotIn(ModNPC.gunther, villager_names) - self.assertNotIn(ModNPC.martin, villager_names) - self.assertNotIn(ModNPC.marlon, villager_names) - self.assertNotIn(ModNPC.morgan, villager_names) - self.assertNotIn(ModNPC.morris, villager_names) - self.assertNotIn(ModNPC.scarlett, villager_names) - self.assertNotIn(ModNPC.susan, villager_names) - self.assertNotIn(ModNPC.goblin, villager_names) - self.assertNotIn(ModNPC.alecto, villager_names) - - def test_sve_has_sve_villagers(self): - villagers = get_villagers_for_mods(sve) - villager_names = {villager.name for villager in villagers} - - self.assertIn(ModNPC.lance, villager_names) - self.assertIn(ModNPC.apples, villager_names) - self.assertIn(ModNPC.claire, villager_names) - self.assertIn(ModNPC.olivia, villager_names) - self.assertIn(ModNPC.sophia, villager_names) - self.assertIn(ModNPC.victor, villager_names) - self.assertIn(ModNPC.andy, villager_names) - self.assertIn(ModNPC.gunther, villager_names) - self.assertIn(ModNPC.martin, villager_names) - self.assertIn(ModNPC.marlon, villager_names) - self.assertIn(ModNPC.morgan, villager_names) - self.assertIn(ModNPC.morris, villager_names) - self.assertIn(ModNPC.scarlett, villager_names) - self.assertIn(ModNPC.susan, villager_names) - - def test_sve_has_no_other_mod_villagers(self): - villagers = get_villagers_for_mods(sve) - villager_names = {villager.name for villager in villagers} - - self.assertNotIn(ModNPC.alec, villager_names) - self.assertNotIn(ModNPC.ayeisha, villager_names) - self.assertNotIn(ModNPC.delores, villager_names) - self.assertNotIn(ModNPC.eugene, villager_names) - self.assertNotIn(ModNPC.jasper, villager_names) - self.assertNotIn(ModNPC.juna, villager_names) - self.assertNotIn(ModNPC.mr_ginger, villager_names) - self.assertNotIn(ModNPC.riley, villager_names) - self.assertNotIn(ModNPC.shiko, villager_names) - self.assertNotIn(ModNPC.wellwick, villager_names) - self.assertNotIn(ModNPC.yoba, villager_names) - self.assertNotIn(ModNPC.goblin, villager_names) - self.assertNotIn(ModNPC.alecto, villager_names) - - def test_no_mods_wizard_is_not_bachelor(self): - villagers = get_villagers_for_mods(no_mods) - villagers_by_name = {villager.name: villager for villager in villagers} - self.assertFalse(villagers_by_name[NPC.wizard].bachelor) - self.assertEqual(villagers_by_name[NPC.wizard].mod_name, ModNames.vanilla) - - def test_sve_wizard_is_bachelor(self): - villagers = get_villagers_for_mods(sve) - villagers_by_name = {villager.name: villager for villager in villagers} - self.assertTrue(villagers_by_name[NPC.wizard].bachelor) - self.assertEqual(villagers_by_name[NPC.wizard].mod_name, ModNames.sve) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 57bca5f25645..89f82870e4a7 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -1,48 +1,51 @@ import random from BaseClasses import get_seed -from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods, complete_options_with_default +from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, \ + fill_dataclass_with_default from ..assertion import ModAssertMixin, WorldAssertMixin -from ... import items, Group, ItemClassification +from ... import items, Group, ItemClassification, create_content from ... import options from ...items import items_by_group -from ...mods.mod_data import all_mods +from ...options import SkillProgression, Walnutsanity from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): - for mod in all_mods: - with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}, dirty_state=True) as (multi_world, _): + for mod in options.Mods.valid_keys: + world_options = {options.Mods: mod, options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false} + with self.solo_world_sub_test(f"Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self): for option in options.EntranceRandomization.options: - for mod in all_mods: + for mod in options.Mods.valid_keys: world_options = { - options.EntranceRandomization.internal_name: options.EntranceRandomization.options[option], - options.Mods: mod + options.EntranceRandomization: options.EntranceRandomization.options[option], + options.Mods: mod, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false } - with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options, dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) def test_allsanity_all_mods_when_generate_then_basic_checks(self): - with self.solo_world_sub_test(world_options=allsanity_options_with_mods(), dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _): self.assert_basic_checks(multi_world) def test_allsanity_all_mods_exclude_island_when_generate_then_basic_checks(self): - world_options = allsanity_options_with_mods() + world_options = allsanity_mods_6_x_x() world_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true}) - with self.solo_world_sub_test(world_options=world_options, dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(world_options=world_options) as (multi_world, _): self.assert_basic_checks(multi_world) class TestBaseLocationDependencies(SVTestBase): options = { - options.Mods.internal_name: all_mods, + options.Mods.internal_name: frozenset(options.Mods.valid_keys), options.ToolProgression.internal_name: options.ToolProgression.option_progressive, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized } @@ -50,13 +53,17 @@ class TestBaseLocationDependencies(SVTestBase): class TestBaseItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.Shipsanity.internal_name: options.Shipsanity.option_everything, options.Chefsanity.internal_name: options.Chefsanity.option_all, options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Mods.internal_name: all_mods + options.Booksanity.internal_name: options.Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) } def test_all_progression_items_are_added_to_the_pool(self): @@ -69,7 +76,7 @@ def test_all_progression_items_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): @@ -78,13 +85,15 @@ def test_all_progression_items_are_added_to_the_pool(self): class TestNoGingerIslandModItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.Shipsanity.internal_name: options.Shipsanity.option_everything, options.Chefsanity.internal_name: options.Chefsanity.option_all, options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Mods.internal_name: all_mods + options.Booksanity.internal_name: options.Booksanity.option_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -97,7 +106,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): @@ -112,18 +121,21 @@ class TestModEntranceRando(SVTestCase): def test_mod_entrance_randomization(self): for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: - sv_options = complete_options_with_default({ + sv_options = fill_dataclass_with_default({ options.EntranceRandomization.internal_name: option, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.Mods.internal_name: all_mods + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) }) + content = create_content(sv_options) seed = get_seed() rand = random.Random(seed) with self.subTest(option=option, flag=flag, seed=seed): final_connections, final_regions = create_final_connections_and_regions(sv_options) - _, randomized_connections = randomize_connections(rand, sv_options, final_regions, final_connections) + _, randomized_connections = randomize_connections(rand, sv_options, content, final_regions, final_connections) for connection_name in final_connections: connection = final_connections[connection_name] @@ -143,11 +155,11 @@ def test_given_traps_when_generate_then_all_traps_in_pool(self): if value == "no_traps": continue - world_options = allsanity_options_without_mods() - world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods: "Magic"}) - multi_world = setup_solo_multiworld(world_options) - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] - multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"Option: {value}, Item: {item}"): - self.assertIn(item, multiworld_items) + world_options = allsanity_no_mods_6_x_x() + world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods.internal_name: "Magic"}) + with solo_multiworld(world_options) as (multi_world, _): + trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] + multiworld_items = [item.name for item in multi_world.get_items()] + for item in trap_items: + with self.subTest(f"Option: {value}, Item: {item}"): + self.assertIn(item, multiworld_items) diff --git a/worlds/stardew_valley/test/options/TestForcedOptions.py b/worlds/stardew_valley/test/options/TestForcedOptions.py new file mode 100644 index 000000000000..c32def6c6ca8 --- /dev/null +++ b/worlds/stardew_valley/test/options/TestForcedOptions.py @@ -0,0 +1,115 @@ +import itertools +import unittest + +import Options as ap_options +from .utils import fill_dataclass_with_default +from ... import options +from ...options.forced_options import force_change_options_if_incompatible + + +class TestGoalsRequiringAllLocationsOverrideAccessibility(unittest.TestCase): + + def test_given_goal_requiring_all_locations_when_generate_then_accessibility_is_forced_to_full(self): + """There is a bug with the current victory condition of the perfection goal that can create unwinnable seeds if the accessibility is set to minimal and + the world gets flooded with progression items through plando. This will increase the amount of collected progression items pass the total amount + calculated for the world when creating the item pool. This will cause the victory condition to be met before all locations are collected, so some could + be left inaccessible, which in practice will make the seed unwinnable. + """ + + for goal in [options.Goal.option_perfection, options.Goal.option_allsanity]: + for accessibility in ap_options.Accessibility.options.keys(): + with self.subTest(f"Goal: {options.Goal.get_option_name(goal)} Accessibility: {accessibility}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + "accessibility": accessibility + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.accessibility.value, ap_options.Accessibility.option_full) + + +class TestGingerIslandRelatedGoalsOverrideGingerIslandExclusion(unittest.TestCase): + + def test_given_island_related_goal_when_generate_then_override_exclude_ginger_island(self): + for goal in [options.Goal.option_greatest_walnut_hunter, options.Goal.option_perfection]: + for exclude_island in options.ExcludeGingerIsland.options: + with self.subTest(f"Goal: {options.Goal.get_option_name(goal)} Exclude Ginger Island: {exclude_island}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + options.ExcludeGingerIsland: exclude_island + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.exclude_ginger_island.value, options.ExcludeGingerIsland.option_false) + + +class TestGingerIslandExclusionOverridesWalnutsanity(unittest.TestCase): + + def test_given_ginger_island_excluded_when_generate_then_walnutsanity_is_forced_disabled(self): + walnutsanity_options = options.Walnutsanity.valid_keys + for walnutsanity in ( + walnutsanity + for r in range(len(walnutsanity_options) + 1) + for walnutsanity in itertools.combinations(walnutsanity_options, r) + ): + with self.subTest(f"Walnutsanity: {walnutsanity}"): + world_options = fill_dataclass_with_default({ + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.Walnutsanity: walnutsanity + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.walnutsanity.value, options.Walnutsanity.preset_none) + + def test_given_ginger_island_related_goal_and_ginger_island_excluded_when_generate_then_walnutsanity_is_not_changed(self): + for goal in [options.Goal.option_greatest_walnut_hunter, options.Goal.option_perfection]: + walnutsanity_options = options.Walnutsanity.valid_keys + for original_walnutsanity_choice in ( + set(walnutsanity) + for r in range(len(walnutsanity_options) + 1) + for walnutsanity in itertools.combinations(walnutsanity_options, r) + ): + with self.subTest(f"Goal: {options.Goal.get_option_name(goal)} Walnutsanity: {original_walnutsanity_choice}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.Walnutsanity: original_walnutsanity_choice + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.walnutsanity.value, original_walnutsanity_choice) + + +class TestGingerIslandExclusionOverridesQisSpecialOrders(unittest.TestCase): + + def test_given_ginger_island_excluded_when_generate_then_qis_special_orders_are_forced_disabled(self): + special_order_options = options.SpecialOrderLocations.options + for special_order in special_order_options.keys(): + with self.subTest(f"Special order: {special_order}"): + world_options = fill_dataclass_with_default({ + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.SpecialOrderLocations: special_order + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.special_order_locations.value & options.SpecialOrderLocations.value_qi, 0) + + def test_given_ginger_island_related_goal_and_ginger_island_excluded_when_generate_then_special_orders_is_not_changed(self): + for goal in [options.Goal.option_greatest_walnut_hunter, options.Goal.option_perfection]: + special_order_options = options.SpecialOrderLocations.options + for special_order, original_special_order_value in special_order_options.items(): + with self.subTest(f"Special order: {special_order}"): + world_options = fill_dataclass_with_default({ + options.Goal: goal, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.SpecialOrderLocations: special_order + }) + + force_change_options_if_incompatible(world_options, 1, "Tester") + + self.assertEqual(world_options.special_order_locations.value, original_special_order_value) diff --git a/worlds/stardew_valley/test/TestPresets.py b/worlds/stardew_valley/test/options/TestPresets.py similarity index 86% rename from worlds/stardew_valley/test/TestPresets.py rename to worlds/stardew_valley/test/options/TestPresets.py index 2bb1c7fbaeaf..9384acd77060 100644 --- a/worlds/stardew_valley/test/TestPresets.py +++ b/worlds/stardew_valley/test/options/TestPresets.py @@ -1,9 +1,7 @@ -import builtins -import inspect - from Options import PerGameCommonOptions, OptionSet -from . import SVTestCase -from .. import sv_options_presets, StardewValleyOptions +from .. import SVTestCase +from ...options import StardewValleyOptions +from ...options.presets import sv_options_presets class TestPresets(SVTestCase): @@ -18,4 +16,4 @@ def test_all_presets_explicitly_set_all_options(self): with self.subTest(f"{preset_name}"): for option_name in mandatory_option_names: with self.subTest(f"{preset_name} -> {option_name}"): - self.assertIn(option_name, sv_options_presets[preset_name]) \ No newline at end of file + self.assertIn(option_name, sv_options_presets[preset_name]) diff --git a/worlds/stardew_valley/test/options/__init__.py b/worlds/stardew_valley/test/options/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/options/utils.py b/worlds/stardew_valley/test/options/utils.py new file mode 100644 index 000000000000..9f02105da84f --- /dev/null +++ b/worlds/stardew_valley/test/options/utils.py @@ -0,0 +1,68 @@ +from argparse import Namespace +from typing import Any, Iterable + +from BaseClasses import PlandoOptions +from Options import VerifyKeys +from ... import StardewValleyWorld +from ...options import StardewValleyOptions, StardewValleyOption + + +def parse_class_option_keys(test_options: dict[str | StardewValleyOption, Any] | None) -> dict: + """ Now the option class is allowed as key. """ + if test_options is None: + return {} + parsed_options = {} + + for option, value in test_options.items(): + if hasattr(option, "internal_name"): + assert option.internal_name not in test_options, "Defined two times by class and internal_name" + parsed_options[option.internal_name] = value + else: + assert option in StardewValleyOptions.type_hints, \ + f"All keys of world_options must be a possible Stardew Valley option, {option} is not." + parsed_options[option] = value + + return parsed_options + + +def fill_dataclass_with_default(test_options: dict[str | StardewValleyOption, Any] | None) -> StardewValleyOptions: + test_options = parse_class_option_keys(test_options) + + filled_options = {} + for option_name, option_class in StardewValleyOptions.type_hints.items(): + + value = option_class.from_any(test_options.get(option_name, option_class.default)) + + if issubclass(option_class, VerifyKeys): + # Values should already be verified, but just in case... + value.verify(StardewValleyWorld, "Tester", PlandoOptions.bosses) + + filled_options[option_name] = value + + return StardewValleyOptions(**filled_options) + + +def fill_namespace_with_default(test_options: dict[str, Any] | Iterable[dict[str, Any]]) -> Namespace: + if isinstance(test_options, dict): + test_options = [test_options] + + args = Namespace() + for option_name, option_class in StardewValleyOptions.type_hints.items(): + all_players_option = {} + + for player_id, player_options in enumerate(test_options): + # Player id starts at 1 + player_id += 1 + player_name = f"Tester{player_id}" + + value = option_class.from_any(player_options.get(option_name, option_class.default)) + + if issubclass(option_class, VerifyKeys): + # Values should already be verified, but just in case... + value.verify(StardewValleyWorld, player_name, PlandoOptions.bosses) + + all_players_option[player_id] = value + + setattr(args, option_name, all_players_option) + + return args diff --git a/worlds/stardew_valley/test/performance/TestPerformance.py b/worlds/stardew_valley/test/performance/TestPerformance.py index 0d453942c35f..b5ad0cae66c6 100644 --- a/worlds/stardew_valley/test/performance/TestPerformance.py +++ b/worlds/stardew_valley/test/performance/TestPerformance.py @@ -8,13 +8,10 @@ from BaseClasses import get_seed from Fill import distribute_items_restrictive, balance_multiworld_progression from worlds import AutoWorld -from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_4_x_x_options, \ - allsanity_4_x_x_options_without_mods, default_options, allsanity_options_without_mods, allsanity_options_with_mods +from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_6_x_x, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x -assert default_4_x_x_options -assert allsanity_4_x_x_options_without_mods -assert default_options -assert allsanity_options_without_mods +assert default_6_x_x +assert allsanity_no_mods_6_x_x default_number_generations = 25 acceptable_deviation = 4 @@ -45,8 +42,6 @@ class SVPerformanceTestCase(SVTestCase): acceptable_time_per_player: float results: List[PerformanceResults] - # Set False to run tests that take long - skip_performance_tests: bool = True # Set False to not call the fill in the tests""" skip_fill: bool = True # Set True to print results as CSV""" @@ -54,10 +49,11 @@ class SVPerformanceTestCase(SVTestCase): @classmethod def setUpClass(cls) -> None: - super().setUpClass() performance_tests_key = "performance" - if performance_tests_key in os.environ: - cls.skip_performance_tests = not bool(os.environ[performance_tests_key]) + if performance_tests_key not in os.environ or os.environ[performance_tests_key] != "True": + raise unittest.SkipTest("Performance tests disabled") + + super().setUpClass() fill_tests_key = "fill" if fill_tests_key in os.environ: @@ -102,7 +98,7 @@ def performance_test_multiworld(self, options): acceptable_average_time = self.acceptable_time_per_player * amount_of_players total_time = 0 all_times = [] - seeds = [get_seed() for _ in range(self.number_generations)] if not self.fixed_seed else [87876703343494157696] * self.number_generations + seeds = [get_seed() for _ in range(self.number_generations)] if not self.fixed_seed else [85635032403287291967] * self.number_generations for i, seed in enumerate(seeds): with self.subTest(f"Seed: {seed}"): @@ -139,38 +135,26 @@ def size_name(number_players): class TestDefaultOptions(SVPerformanceTestCase): acceptable_time_per_player = 2 - options = default_options() + options = default_6_x_x() results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -182,33 +166,21 @@ class TestMinLocationMaxItems(SVPerformanceTestCase): results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -216,39 +188,27 @@ def test_10_player(self): class TestAllsanityWithoutMods(SVPerformanceTestCase): acceptable_time_per_player = 10 - options = allsanity_options_without_mods() + options = allsanity_no_mods_6_x_x() results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -256,21 +216,17 @@ def test_10_player(self): class TestAllsanityWithMods(SVPerformanceTestCase): acceptable_time_per_player = 25 - options = allsanity_options_with_mods() + options = allsanity_mods_6_x_x() results = [] + @unittest.skip def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) + @unittest.skip def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) diff --git a/worlds/stardew_valley/test/rules/TestArcades.py b/worlds/stardew_valley/test/rules/TestArcades.py new file mode 100644 index 000000000000..69e5b22cc01b --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestArcades.py @@ -0,0 +1,97 @@ +from ... import options +from ...test import SVTestBase + + +class TestArcadeMachinesLogic(SVTestBase): + options = { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + } + + def test_prairie_king(self): + self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + + boots = self.create_item("JotPK: Progressive Boots") + gun = self.create_item("JotPK: Progressive Gun") + ammo = self.create_item("JotPK: Progressive Ammo") + life = self.create_item("JotPK: Extra Life") + drop = self.create_item("JotPK: Increased Drop Rate") + + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + + self.multiworld.state.collect(boots) + self.multiworld.state.collect(boots) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(boots) + + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(life) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + self.remove(ammo) + self.remove(life) + + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(life) + self.multiworld.state.collect(drop) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) + + self.multiworld.state.collect(boots) + self.multiworld.state.collect(boots) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(gun) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(ammo) + self.multiworld.state.collect(life) + self.multiworld.state.collect(drop) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) 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/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py new file mode 100644 index 000000000000..d1f60b20e0db --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBuildings.py @@ -0,0 +1,53 @@ +from ...options import BuildingProgression, FarmType +from ...test import SVTestBase + + +class TestBuildingLogic(SVTestBase): + options = { + FarmType.internal_name: FarmType.option_standard, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + } + + def test_coop_blueprint(self): + self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) + + self.collect_lots_of_money() + self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) + + def test_big_coop_blueprint(self): + big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint") + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.collect_lots_of_money() + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Progressive Coop")) + self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + def test_deluxe_coop_blueprint(self): + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.collect_lots_of_money() + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item("Progressive Coop")) + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item("Progressive Coop")) + self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + def test_big_shed_blueprint(self): + big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint") + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.collect_lots_of_money() + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Progressive Shed")) + self.assertTrue(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py new file mode 100644 index 000000000000..0bc7f9bfdfd4 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -0,0 +1,67 @@ +from ... import options +from ...options import BundleRandomization +from ...strings.bundle_names import BundleName +from ...test import SVTestBase + + +class TestBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_vanilla, + options.BundlePrice: options.BundlePrice.default, + } + + def test_vault_2500g_bundle(self): + self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) + + self.collect_lots_of_money() + self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) + + +class TestRemixedBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_remixed, + options.BundlePrice: options.BundlePrice.default, + options.BundlePlando: frozenset({BundleName.sticky}) + } + + def test_sticky_bundle_has_grind_rules(self): + self.assertFalse(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state)) + + self.collect_all_the_money() + self.assertTrue(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state)) + + +class TestRaccoonBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_vanilla, + options.BundlePrice: options.BundlePrice.option_normal, + options.Craftsanity: options.Craftsanity.option_all, + } + seed = 2 # Magic seed that does what I want. Might need to get changed if we change the randomness behavior of raccoon bundles + + def test_raccoon_bundles_rely_on_previous_ones(self): + # The first raccoon bundle is a fishing one + raccoon_rule_1 = self.world.logic.region.can_reach_location("Raccoon Request 1") + + # The 3th raccoon bundle is a foraging one + raccoon_rule_3 = self.world.logic.region.can_reach_location("Raccoon Request 3") + self.collect("Progressive Raccoon", 6) + self.collect("Progressive Mine Elevator", 24) + self.collect("Mining Level", 12) + self.collect("Combat Level", 12) + self.collect("Progressive Axe", 4) + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Weapon", 4) + self.collect("Dehydrator Recipe") + self.collect("Mushroom Boxes") + self.collect("Progressive Fishing Rod", 4) + self.collect("Fishing Level", 10) + self.collect("Furnace Recipe") + + self.assertFalse(raccoon_rule_1(self.multiworld.state)) + self.assertFalse(raccoon_rule_3(self.multiworld.state)) + + self.collect("Fish Smoker Recipe") + + self.assertTrue(raccoon_rule_1(self.multiworld.state)) + self.assertTrue(raccoon_rule_3(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestCookingRecipes.py b/worlds/stardew_valley/test/rules/TestCookingRecipes.py new file mode 100644 index 000000000000..d5f9da73c9d7 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestCookingRecipes.py @@ -0,0 +1,83 @@ +from ... import options +from ...options import BuildingProgression, ExcludeGingerIsland, Chefsanity +from ...test import SVTestBase + + +class TestRecipeLearnLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + Chefsanity.internal_name: Chefsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_learn_qos_recipe(self): + location = "Cook Radish Salad" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Progressive House")) + self.multiworld.state.collect(self.create_item("Radish Seeds")) + self.multiworld.state.collect(self.create_item("Spring")) + self.multiworld.state.collect(self.create_item("Summer")) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("The Queen of Sauce")) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestRecipeReceiveLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + Chefsanity.internal_name: Chefsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_learn_qos_recipe(self): + location = "Cook Radish Salad" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Progressive House")) + self.multiworld.state.collect(self.create_item("Radish Seeds")) + self.multiworld.state.collect(self.create_item("Summer")) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + spring = self.create_item("Spring") + qos = self.create_item("The Queen of Sauce") + self.multiworld.state.collect(spring) + self.multiworld.state.collect(qos) + self.assert_rule_false(rule, self.multiworld.state) + self.multiworld.state.remove(spring) + self.multiworld.state.remove(qos) + + self.multiworld.state.collect(self.create_item("Radish Salad Recipe")) + self.assert_rule_true(rule, self.multiworld.state) + + def test_get_chefsanity_check_recipe(self): + location = "Radish Salad Recipe" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Spring")) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + seeds = self.create_item("Radish Seeds") + summer = self.create_item("Summer") + house = self.create_item("Progressive House") + self.multiworld.state.collect(seeds) + self.multiworld.state.collect(summer) + self.multiworld.state.collect(house) + self.assert_rule_false(rule, self.multiworld.state) + self.multiworld.state.remove(seeds) + self.multiworld.state.remove(summer) + self.multiworld.state.remove(house) + + self.multiworld.state.collect(self.create_item("The Queen of Sauce")) + self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py new file mode 100644 index 000000000000..46a1b73d0b7a --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -0,0 +1,157 @@ +from ... import options +from ...data.craftable_data import all_crafting_recipes_by_name +from ...options import BuildingProgression, ExcludeGingerIsland, Craftsanity, SeasonRandomization +from ...test import SVTestBase + + +class TestCraftsanityLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_recipe(self): + location = "Craft Marble Brazier" + rule = self.world.logic.region.can_reach_location(location) + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Marble Brazier Recipe")) + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_learn_crafting_recipe(self): + location = "Marble Brazier Recipe" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.collect_lots_of_money() + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) + self.multiworld.state.collect(self.create_item("Torch Recipe")) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Fall")) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe")) + self.assert_rule_true(rule, self.multiworld.state) + + def test_require_furnace_recipe_for_smelting_checks(self): + locations = ["Craft Furnace", "Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Furnace Recipe")) + self.assert_rules_true(rules, self.multiworld.state) + + +class TestCraftsanityWithFestivalsLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) + self.multiworld.state.collect(self.create_item("Fall")) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe")) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Torch Recipe")) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestNoCraftsanityLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + Craftsanity.internal_name: Craftsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_recipe(self): + recipe = all_crafting_recipes_by_name["Wood Floor"] + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + result = rule(self.multiworld.state) + self.assertFalse(result) + + self.collect([self.create_item("Progressive Season")] * 2) + self.assert_rule_true(rule, self.multiworld.state) + + def test_requires_mining_levels_for_smelting_checks(self): + locations = ["Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.multiworld.state.collect(self.create_item("Furnace Recipe")) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.collect([self.create_item("Mining Level")] * 10) + self.assert_rules_true(rules, self.multiworld.state) + + +class TestNoCraftsanityWithFestivalsLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + Craftsanity.internal_name: Craftsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds")) + self.multiworld.state.collect(self.create_item("Fall")) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe")) + self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestDonations.py b/worlds/stardew_valley/test/rules/TestDonations.py new file mode 100644 index 000000000000..3927bd09a48b --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestDonations.py @@ -0,0 +1,73 @@ +from ... import options +from ...locations import locations_by_tag, LocationTags, location_table +from ...strings.entrance_names import Entrance +from ...strings.region_names import Region +from ...test import SVTestBase + + +class TestDonationLogicAll(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_all + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item)) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicRandomized(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_randomized + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + donation_locations = [location for location in self.get_real_locations() if + LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] + + for donation in donation_locations: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item)) + + for donation in donation_locations: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicMilestones(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_milestones + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item)) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +def swap_museum_and_bathhouse(multiworld, player): + museum_region = multiworld.get_region(Region.museum, player) + bathhouse_region = multiworld.get_region(Region.bathhouse_entrance, player) + museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player) + bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) + museum_entrance.connect(bathhouse_region) + bathhouse_entrance.connect(museum_region) 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/TestFriendship.py b/worlds/stardew_valley/test/rules/TestFriendship.py new file mode 100644 index 000000000000..3e9109ed5010 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestFriendship.py @@ -0,0 +1,58 @@ +from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize +from ...test import SVTestBase + + +class TestFriendsanityDatingRules(SVTestBase): + options = { + SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 3 + } + + def test_earning_dating_heart_requires_dating(self): + self.collect_all_the_money() + self.multiworld.state.collect(self.create_item("Fall")) + self.multiworld.state.collect(self.create_item("Beach Bridge")) + self.multiworld.state.collect(self.create_item("Progressive House")) + for i in range(3): + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Weapon")) + self.multiworld.state.collect(self.create_item("Progressive Axe")) + self.multiworld.state.collect(self.create_item("Progressive Barn")) + for i in range(10): + self.multiworld.state.collect(self.create_item("Foraging Level")) + self.multiworld.state.collect(self.create_item("Farming Level")) + self.multiworld.state.collect(self.create_item("Mining Level")) + self.multiworld.state.collect(self.create_item("Combat Level")) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator")) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator")) + + npc = "Abigail" + heart_name = f"{npc} <3" + step = 3 + + self.assert_can_reach_heart_up_to(npc, 3, step) + self.multiworld.state.collect(self.create_item(heart_name)) + self.assert_can_reach_heart_up_to(npc, 6, step) + self.multiworld.state.collect(self.create_item(heart_name)) + self.assert_can_reach_heart_up_to(npc, 8, step) + self.multiworld.state.collect(self.create_item(heart_name)) + self.assert_can_reach_heart_up_to(npc, 10, step) + self.multiworld.state.collect(self.create_item(heart_name)) + self.assert_can_reach_heart_up_to(npc, 14, step) + + def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): + prefix = "Friendsanity: " + suffix = " <3" + for i in range(1, max_reachable + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) + self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts") + for i in range(max_reachable + 1, 14 + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) + self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts") diff --git a/worlds/stardew_valley/test/rules/TestMuseum.py b/worlds/stardew_valley/test/rules/TestMuseum.py new file mode 100644 index 000000000000..35dad8f43ebc --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestMuseum.py @@ -0,0 +1,16 @@ +from collections import Counter + +from ...options import Museumsanity +from .. import SVTestBase + + +class TestMuseumMilestones(SVTestBase): + options = { + Museumsanity.internal_name: Museumsanity.option_milestones + } + + def test_50_milestone(self): + self.multiworld.state.prog_items = {1: Counter()} + + milestone_rule = self.world.logic.museum.can_find_museum_items(50) + self.assert_rule_false(milestone_rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py new file mode 100644 index 000000000000..b26d1e94ee2c --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestShipping.py @@ -0,0 +1,82 @@ +from ...locations import LocationTags, location_table +from ...options import BuildingProgression, Shipsanity +from ...test import SVTestBase + + +class TestShipsanityNone(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_none + } + + def test_no_shipsanity_locations(self): + for location in self.get_real_locations(): + self.assertFalse("Shipsanity" in location.name) + self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) + + +class TestShipsanityCrops(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_crops + } + + def test_only_crop_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) + + +class TestShipsanityFish(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_fish + } + + def test_only_fish_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) + + +class TestShipsanityFullShipment(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_full_shipment + } + + def test_only_full_shipment_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) + self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) + + +class TestShipsanityFullShipmentWithFish(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish + } + + def test_only_full_shipment_and_fish_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or + LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) + + +class TestShipsanityEverything(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_everything, + BuildingProgression.internal_name: BuildingProgression.option_progressive + } + + def test_all_shipsanity_locations_require_shipping_bin(self): + bin_name = "Shipping Bin" + self.collect_all_except(bin_name) + shipsanity_locations = [location for location in self.get_real_locations() if + LocationTags.SHIPSANITY in location_table[location.name].tags] + bin_item = self.create_item(bin_name) + for location in shipsanity_locations: + with self.subTest(location.name): + self.remove(bin_item) + self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) + self.multiworld.state.collect(bin_item) + shipsanity_rule = self.world.logic.region.can_reach_location(location.name) + self.assert_rule_true(shipsanity_rule, self.multiworld.state) + self.remove(bin_item) diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py new file mode 100644 index 000000000000..77adade886dc --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -0,0 +1,113 @@ +from ... import HasProgressionPercent, StardewLogic +from ...options import ToolProgression, SkillProgression, Mods +from ...strings.skill_names import all_skills, all_vanilla_skills, Skill +from ...test import 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(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) + + self.assertIn(time_rule, rule.current_rules) + + +class TestSkillProgressionProgressive(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_progressive, + Mods.internal_name: frozenset(Mods.valid_keys), + } + + 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): + if level > 1: + self.assert_reach_location_false(location, self.multiworld.state) + self.collect(f"{skill} Level") + + 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/stardew_valley/test/rules/TestStateRules.py b/worlds/stardew_valley/test/rules/TestStateRules.py new file mode 100644 index 000000000000..49577d2223e0 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestStateRules.py @@ -0,0 +1,22 @@ +from .. import SVTestBase, allsanity_mods_6_x_x +from ...stardew_rule import HasProgressionPercent + + +class TestHasProgressionPercentWithVictory(SVTestBase): + options = allsanity_mods_6_x_x() + + def test_has_100_progression_percent_is_false_while_items_are_missing(self): + has_100_progression_percent = HasProgressionPercent(1, 100) + + for i, item in enumerate([i for i in self.multiworld.get_items() if i.advancement and i.code][1:]): + if item.name != "Victory": + self.collect(item) + self.assertFalse(has_100_progression_percent(self.multiworld.state), + f"Rule became true after {i} items, total_progression_items is {self.world.total_progression_items}") + + def test_has_100_progression_percent_account_for_victory_not_being_collected(self): + has_100_progression_percent = HasProgressionPercent(1, 100) + + self.collect_all_except("Victory") + + self.assert_rule_true(has_100_progression_percent, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestTools.py b/worlds/stardew_valley/test/rules/TestTools.py new file mode 100644 index 000000000000..5b8975f4e707 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestTools.py @@ -0,0 +1,141 @@ +from collections import Counter + +from .. import SVTestBase +from ... import Event, options +from ...options import ToolProgression, SeasonRandomization +from ...strings.entrance_names import Entrance +from ...strings.region_names import Region +from ...strings.tool_names import Tool, ToolMaterial + + +class TestProgressiveToolsLogic(SVTestBase): + options = { + ToolProgression.internal_name: ToolProgression.option_progressive, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + } + + def test_sturgeon(self): + self.multiworld.state.prog_items = {1: Counter()} + + sturgeon_rule = self.world.logic.has("Sturgeon") + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + summer = self.create_item("Summer") + self.multiworld.state.collect(summer) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + fishing_rod = self.create_item("Progressive Fishing Rod") + self.multiworld.state.collect(fishing_rod) + self.multiworld.state.collect(fishing_rod) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + fishing_level = self.create_item("Fishing Level") + self.multiworld.state.collect(fishing_level) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) + self.multiworld.state.collect(fishing_level) + self.assert_rule_true(sturgeon_rule, self.multiworld.state) + + self.remove(summer) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + winter = self.create_item("Winter") + self.multiworld.state.collect(winter) + self.assert_rule_true(sturgeon_rule, self.multiworld.state) + + self.remove(fishing_rod) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + def test_old_master_cannoli(self): + self.multiworld.state.prog_items = {1: Counter()} + + self.multiworld.state.collect(self.create_item("Progressive Axe")) + self.multiworld.state.collect(self.create_item("Progressive Axe")) + self.multiworld.state.collect(self.create_item("Summer")) + self.collect_lots_of_money() + + rule = self.world.logic.region.can_reach_location("Old Master Cannoli") + self.assert_rule_false(rule, self.multiworld.state) + + fall = self.create_item("Fall") + self.multiworld.state.collect(fall) + self.assert_rule_false(rule, self.multiworld.state) + + tuesday = self.create_item("Traveling Merchant: Tuesday") + self.multiworld.state.collect(tuesday) + self.assert_rule_false(rule, self.multiworld.state) + + rare_seed = self.create_item("Rare Seed") + self.multiworld.state.collect(rare_seed) + self.assert_rule_true(rule, self.multiworld.state) + + self.remove(fall) + self.remove(self.create_item(Event.fall_farming)) + self.assert_rule_false(rule, self.multiworld.state) + self.remove(tuesday) + + green_house = self.create_item("Greenhouse") + self.collect(self.create_item(Event.fall_farming)) + self.multiworld.state.collect(green_house) + self.assert_rule_false(rule, self.multiworld.state) + + friday = self.create_item("Traveling Merchant: Friday") + self.multiworld.state.collect(friday) + self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) + + self.remove(green_house) + self.remove(self.create_item(Event.fall_farming)) + self.assert_rule_false(rule, self.multiworld.state) + self.remove(friday) + + +class TestToolVanillaRequiresBlacksmith(SVTestBase): + options = { + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.ToolProgression: options.ToolProgression.option_vanilla, + } + seed = 4111845104987680262 + + # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. + + def test_cannot_get_any_tool_without_blacksmith_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) + self.collect_all_except(railroad_item) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + self.multiworld.state.collect(self.create_item(railroad_item)) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + def test_cannot_get_fishing_rod_without_willy_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) + self.collect_all_except(railroad_item) + + for fishing_rod_level in [3, 4]: + self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + self.multiworld.state.collect(self.create_item(railroad_item)) + + for fishing_rod_level in [3, 4]: + self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + +def place_region_at_entrance(multiworld, player, region, entrance): + region_to_place = multiworld.get_region(region, player) + entrance_to_place_region = multiworld.get_entrance(entrance, player) + + entrance_to_switch = region_to_place.entrances[0] + region_to_switch = entrance_to_place_region.connected_region + entrance_to_switch.connect(region_to_switch) + entrance_to_place_region.connect(region_to_place) diff --git a/worlds/stardew_valley/test/rules/TestWeapons.py b/worlds/stardew_valley/test/rules/TestWeapons.py new file mode 100644 index 000000000000..383f26e841d2 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestWeapons.py @@ -0,0 +1,75 @@ +from ... import options +from ...options import ToolProgression +from ...test import SVTestBase + + +class TestWeaponsLogic(SVTestBase): + options = { + ToolProgression.internal_name: ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_mine(self): + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe")) + self.multiworld.state.collect(self.create_item("Progressive House")) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.multiworld.state.collect(self.create_item("Bus Repair")) + self.multiworld.state.collect(self.create_item("Skull Key")) + + self.give_item_and_check_reachable_mine("Progressive Sword", 1) + self.give_item_and_check_reachable_mine("Progressive Dagger", 1) + self.give_item_and_check_reachable_mine("Progressive Club", 1) + + self.give_item_and_check_reachable_mine("Progressive Sword", 2) + self.give_item_and_check_reachable_mine("Progressive Dagger", 2) + self.give_item_and_check_reachable_mine("Progressive Club", 2) + + self.give_item_and_check_reachable_mine("Progressive Sword", 3) + self.give_item_and_check_reachable_mine("Progressive Dagger", 3) + self.give_item_and_check_reachable_mine("Progressive Club", 3) + + self.give_item_and_check_reachable_mine("Progressive Sword", 4) + self.give_item_and_check_reachable_mine("Progressive Dagger", 4) + self.give_item_and_check_reachable_mine("Progressive Club", 4) + + self.give_item_and_check_reachable_mine("Progressive Sword", 5) + self.give_item_and_check_reachable_mine("Progressive Dagger", 5) + self.give_item_and_check_reachable_mine("Progressive Club", 5) + + def give_item_and_check_reachable_mine(self, item_name: str, reachable_level: int): + item = self.multiworld.create_item(item_name, self.player) + self.multiworld.state.collect(item) + rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40() + if reachable_level > 0: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_mines_floor_41_80() + if reachable_level > 1: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_mines_floor_81_120() + if reachable_level > 2: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_skull_cavern() + if reachable_level > 3: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.ability.can_mine_perfectly_in_the_skull_cavern() + if reachable_level > 4: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/__init__.py b/worlds/stardew_valley/test/rules/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/script/__init__.py b/worlds/stardew_valley/test/script/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/script/benchmark_locations.py b/worlds/stardew_valley/test/script/benchmark_locations.py new file mode 100644 index 000000000000..04553e39968e --- /dev/null +++ b/worlds/stardew_valley/test/script/benchmark_locations.py @@ -0,0 +1,140 @@ +""" +Copy of the script in test/benchmark, adapted to Stardew Valley. + +Run with `python -m worlds.stardew_valley.test.script.benchmark_locations --options minimal_locations_maximal_items` +""" + +import argparse +import collections +import gc +import logging +import os +import sys +import time +import typing + +from BaseClasses import CollectionState, Location +from Utils import init_logging +from worlds.stardew_valley.stardew_rule.rule_explain import explain +from ... import test + + +def run_locations_benchmark(): + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + class BenchmarkRunner: + gen_steps: typing.Tuple[str, ...] = ( + "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + rule_iterations: int = 100_000 + + @staticmethod + def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + + def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: + with TimeIt(f"{test_location.game} {self.rule_iterations} " + f"runs of {test_location}.access_rule({state_name})", logger) as t: + for _ in range(self.rule_iterations): + test_location.access_rule(state) + # if time is taken to disentangle complex ref chains, + # this time should be attributed to the rule. + gc.collect() + return t.dif + + def main(self): + game = "Stardew Valley" + summary_data: typing.Dict[str, collections.Counter[str]] = { + "empty_state": collections.Counter(), + "all_state": collections.Counter(), + } + try: + parser = argparse.ArgumentParser() + parser.add_argument('--options', help="Define the option set to use, from the preset in test/__init__.py .", type=str, required=True) + parser.add_argument('--seed', help="Define the seed to use.", type=int, required=True) + parser.add_argument('--location', help="Define the specific location to benchmark.", type=str, default=None) + parser.add_argument('--state', help="Define the state in which the location will be benchmarked.", type=str, default=None) + args = parser.parse_args() + options_set = args.options + options = getattr(test, options_set)() + seed = args.seed + location = args.location + state = args.state + + multiworld = test.setup_solo_multiworld(options, seed) + gc.collect() + + if location: + locations = [multiworld.get_location(location, 1)] + else: + locations = sorted(multiworld.get_unfilled_locations()) + + all_state = multiworld.get_all_state(False) + for location in locations: + if state != "all_state": + time_taken = self.location_test(location, multiworld.state, "empty_state") + summary_data["empty_state"][location.name] = time_taken + + if state != "empty_state": + time_taken = self.location_test(location, all_state, "all_state") + summary_data["all_state"][location.name] = time_taken + + total_empty_state = sum(summary_data["empty_state"].values()) + total_all_state = sum(summary_data["all_state"].values()) + + logger.info(f"{game} took {total_empty_state / len(locations):.4f} " + f"seconds per location in empty_state and {total_all_state / len(locations):.4f} " + f"in all_state. (all times summed for {self.rule_iterations} runs.)") + logger.info(f"Top times in empty_state:\n" + f"{self.format_times_from_counter(summary_data['empty_state'])}") + logger.info(f"Top times in all_state:\n" + f"{self.format_times_from_counter(summary_data['all_state'])}") + + if len(locations) == 1: + logger.info(str(explain(locations[0].access_rule, all_state, False))) + + except Exception as e: + logger.exception(e) + + runner = BenchmarkRunner() + runner.main() + + +class TimeIt: + def __init__(self, name: str, time_logger=None): + self.name = name + self.logger = time_logger + self.timer = None + self.end_timer = None + + def __enter__(self): + self.timer = time.perf_counter() + return self + + @property + def dif(self): + return self.end_timer - self.timer + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.end_timer: + self.end_timer = time.perf_counter() + if self.logger: + self.logger.info(f"{self.dif:.4f} seconds in {self.name}.") + + +def change_home(): + """Allow scripts to run from "this" folder.""" + old_home = os.path.dirname(__file__) + sys.path.remove(old_home) + new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.chdir(new_home) + sys.path.append(new_home) + # fallback to local import + sys.path.append(old_home) + + from Utils import local_path + local_path.cached_path = new_home + + +if __name__ == "__main__": + run_locations_benchmark() diff --git a/worlds/stardew_valley/test/stability/StabilityOutputScript.py b/worlds/stardew_valley/test/stability/StabilityOutputScript.py index baf17dde8423..c8918d6cf2e1 100644 --- a/worlds/stardew_valley/test/stability/StabilityOutputScript.py +++ b/worlds/stardew_valley/test/stability/StabilityOutputScript.py @@ -1,7 +1,8 @@ import argparse import json -from ...test import setup_solo_multiworld, allsanity_options_with_mods +from ...options import FarmType, EntranceRandomization +from ...test import setup_solo_multiworld, allsanity_mods_6_x_x if __name__ == "__main__": parser = argparse.ArgumentParser() @@ -10,21 +11,23 @@ args = parser.parse_args() seed = args.seed - multi_world = setup_solo_multiworld( - allsanity_options_with_mods(), - seed=seed - ) + options = allsanity_mods_6_x_x() + options[FarmType.internal_name] = FarmType.option_standard + options[EntranceRandomization.internal_name] = EntranceRandomization.option_buildings + multi_world = setup_solo_multiworld(options, seed=seed) + world = multi_world.worlds[1] output = { "bundles": { bundle_room.name: { bundle.name: str(bundle.items) for bundle in bundle_room.bundles } - for bundle_room in multi_world.worlds[1].modified_bundles + for bundle_room in world.modified_bundles }, "items": [item.name for item in multi_world.get_items()], - "location_rules": {location.name: repr(location.access_rule) for location in multi_world.get_locations(1)} + "location_rules": {location.name: repr(location.access_rule) for location in multi_world.get_locations(1)}, + "slot_data": world.fill_slot_data() } print(json.dumps(output)) diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index 48cd663cb301..b4d0f30ea51f 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -2,14 +2,13 @@ import re import subprocess import sys +import unittest from BaseClasses import get_seed from .. import SVTestCase # at 0x102ca98a0> lambda_regex = re.compile(r"^ at (.*)>$") -# Python 3.10.2\r\n -python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$") class TestGenerationIsStable(SVTestCase): @@ -18,9 +17,8 @@ class TestGenerationIsStable(SVTestCase): def test_all_locations_and_items_are_the_same_between_two_generations(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") - # seed = get_seed(33778671150797368040) # troubleshooting seed seed = get_seed() output_a = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) @@ -50,3 +48,6 @@ def test_all_locations_and_items_are_the_same_between_two_generations(self): # We check that the actual rule has the same order to make sure it is evaluated in the same order, # so performance tests are repeatable as much as possible. self.assertEqual(rule_a, rule_b, f"Location rule of {location_a} at index {i} is different between both executions. Seed={seed}") + + for key, value in result_a["slot_data"].items(): + self.assertEqual(value, result_b["slot_data"][key], f"Slot data {key} is different between both executions. Seed={seed}") diff --git a/worlds/stardew_valley/test/stability/TestUniversalTracker.py b/worlds/stardew_valley/test/stability/TestUniversalTracker.py new file mode 100644 index 000000000000..4655b37adf07 --- /dev/null +++ b/worlds/stardew_valley/test/stability/TestUniversalTracker.py @@ -0,0 +1,52 @@ +import unittest +from unittest.mock import Mock + +from .. import SVTestBase, allsanity_mods_6_x_x, fill_namespace_with_default +from ... import STARDEW_VALLEY, FarmType, BundleRandomization, EntranceRandomization + + +class TestUniversalTrackerGenerationIsStable(SVTestBase): + options = allsanity_mods_6_x_x() + options.update({ + EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + BundleRandomization.internal_name: BundleRandomization.option_shuffled, + FarmType.internal_name: FarmType.option_standard, # Need to choose one otherwise it's random + }) + + def test_all_locations_and_items_are_the_same_between_two_generations(self): + # This might open a kivy window temporarily, but it's the only way to test this... + if self.skip_long_tests: + raise unittest.SkipTest("Long tests disabled") + + try: + # This test only run if UT is present, so no risk of running in the CI. + from worlds.tracker.TrackerClient import TrackerGameContext # noqa + except ImportError: + raise unittest.SkipTest("UT not loaded, skipping test") + + slot_data = self.world.fill_slot_data() + ut_data = self.world.interpret_slot_data(slot_data) + + fake_context = Mock() + fake_context.re_gen_passthrough = {STARDEW_VALLEY: ut_data} + args = fill_namespace_with_default({0: self.options}) + args.outputpath = None + args.outputname = None + args.multi = 1 + args.race = None + args.plando_options = self.multiworld.plando_options + args.plando_items = self.multiworld.plando_items + args.plando_texts = self.multiworld.plando_texts + args.plando_connections = self.multiworld.plando_connections + args.game = self.multiworld.game + args.name = self.multiworld.player_name + args.sprite = {} + args.sprite_pool = {} + args.skip_output = True + + generated_multi_world = TrackerGameContext.TMain(fake_context, args, self.multiworld.seed) + generated_slot_data = generated_multi_world.worlds[1].fill_slot_data() + + # Just checking slot data should prove that UT generates the same result as AP generation. + self.maxDiff = None + self.assertEqual(slot_data, generated_slot_data) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 856117469e55..c3cf40a7c010 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,8 +44,8 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - required_client_version = (0, 4, 1) - + 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/subnautica/options.py b/worlds/subnautica/options.py index 4bdd9aafa53f..6cdcb33d8954 100644 --- a/worlds/subnautica/options.py +++ b/worlds/subnautica/options.py @@ -112,8 +112,7 @@ def get_pool(self) -> typing.List[str]: class SubnauticaDeathLink(DeathLink): - """When you die, everyone dies. Of course the reverse is true too. - Note: can be toggled via in-game console command "deathlink".""" + __doc__ = DeathLink.__doc__ + "\n\n Note: can be toggled via in-game console command \"deathlink\"." class FillerItemsDistribution(ItemDict): diff --git a/worlds/subnautica/rules.py b/worlds/subnautica/rules.py index 3b6c5cd4dd68..ea9ec6a8058f 100644 --- a/worlds/subnautica/rules.py +++ b/worlds/subnautica/rules.py @@ -150,7 +150,7 @@ def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool: def get_max_swim_depth(state: "CollectionState", player: int) -> int: - swim_rule: SwimRule = state.multiworld.swim_rule[player] + swim_rule: SwimRule = state.multiworld.worlds[player].options.swim_rule depth: int = swim_rule.base_depth if swim_rule.consider_items: if has_seaglide(state, player): @@ -296,7 +296,7 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(multiworld, player, loc) if subnautica_world.creatures_to_scan: - option = multiworld.creature_scan_logic[player] + option = multiworld.worlds[player].options.creature_scan_logic for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(multiworld, player, creature_name) diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index 45c67c254736..3beead95153b 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -138,7 +138,7 @@ class ItemData(NamedTuple): 'Elevator Keycard': ItemData('Relic', 1337125, progression=True), 'Jewelry Box': ItemData('Relic', 1337126, useful=True), 'Goddess Brooch': ItemData('Relic', 1337127), - 'Wyrm Brooch': ItemData('Relic', 1337128), + 'Wyrm Brooch': ItemData('Relic', 1337128), 'Greed Brooch': ItemData('Relic', 1337129), 'Eternal Brooch': ItemData('Relic', 1337130), 'Blue Orb': ItemData('Orb Melee', 1337131), @@ -199,7 +199,11 @@ class ItemData(NamedTuple): 'Chaos Trap': ItemData('Trap', 1337186, 0, trap=True), 'Neurotoxin Trap': ItemData('Trap', 1337187, 0, trap=True), 'Bee Trap': ItemData('Trap', 1337188, 0, trap=True), - # 1337189 - 1337248 Reserved + 'Laser Access A': ItemData('Relic', 1337189, progression=True), + 'Laser Access I': ItemData('Relic', 1337191, progression=True), + 'Laser Access M': ItemData('Relic', 1337192, progression=True), + 'Throw Stun Trap': ItemData('Trap', 1337193, 0, trap=True), + # 1337194 - 1337248 Reserved 'Max Sand': ItemData('Stat', 1337249, 14) } diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 7b378b4637fa..93ac6ccb98c7 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -1,6 +1,6 @@ from typing import List, Optional, Callable, NamedTuple -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic @@ -14,11 +14,10 @@ class LocationData(NamedTuple): rule: Optional[Callable[[CollectionState], bool]] = None -def get_location_datas(world: Optional[MultiWorld], player: Optional[int], - precalculated_weights: PreCalculatedWeights) -> List[LocationData]: - - flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) +def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]) -> List[LocationData]: + flooded: Optional[PreCalculatedWeights] = precalculated_weights + logic = TimespinnerLogic(player, options, precalculated_weights) # 1337000 - 1337155 Generic locations # 1337171 - 1337175 New Pickup checks @@ -72,8 +71,8 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Skeleton Shaft', 'Sealed Caves (Xarion): Skeleton', 1337044), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Shroom jump room', 1337045, logic.has_timestop), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Double shroom room', 1337046), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Mini jackpot room', 1337047, logic.has_forwarddash_doublejump), - LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Below mini jackpot room', 1337048), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Jacksquat room', 1337047, logic.has_forwarddash_doublejump), + LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Below Jacksquat room', 1337048), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Secret room', 1337049, logic.can_break_walls), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Bottom left room', 1337050), LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Last chance before Xarion', 1337051, logic.has_doublejump), @@ -136,11 +135,11 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], LocationData('Upper Lake Serene', 'Lake Serene: Pyramid keys room', 1337104), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Chicken ledge', 1337174), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Deep dive', 1337105), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), + LocationData('Left Side forest Caves', 'Lake Serene (Lower): Under the eels', 1337106, lambda state: state.has('Water Mask', player)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: flooded.flood_lake_serene or logic.has_doublejump_of_npc(state)), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), + LocationData('Left Side forest Caves', 'Lake Serene (Lower): Past the eels', 1337110, lambda state: state.has('Water Mask', player)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: flooded.flood_maw or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))), @@ -203,12 +202,12 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ] # 1337156 - 1337170 Downloads - if not world or is_option_enabled(world, player, "DownloadableItems"): + if not options or options.downloadable_items: location_table += ( 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)), @@ -223,13 +222,13 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ) # 1337176 - 1337176 Cantoran - if not world or is_option_enabled(world, player, "Cantoran"): + if not options or options.cantoran: location_table += ( LocationData('Left Side forest Caves', 'Lake Serene: Cantoran', 1337176), ) # 1337177 - 1337198 Lore Checks - if not world or is_option_enabled(world, player, "LoreChecks"): + if not options or options.lore_checks: location_table += ( LocationData('Lower lake desolation', 'Lake Desolation: Memory - Coyote Jump (Time Messenger)', 1337177), LocationData('Library', 'Library: Memory - Waterway (A Message)', 1337178), @@ -258,7 +257,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], # 1337199 - 1337236 Reserved for future use # 1337237 - 1337245 GyreArchives - if not world or is_option_enabled(world, player, "GyreArchives"): + if not options or options.gyre_archives: location_table += ( LocationData('Ravenlord\'s Lair', 'Ravenlord: Post fight (pedestal)', 1337237), LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (pedestal)', 1337238), diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py index d316a936b02f..2a0a358737f7 100644 --- a/worlds/timespinner/LogicExtensions.py +++ b/worlds/timespinner/LogicExtensions.py @@ -1,6 +1,6 @@ -from typing import Union -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from typing import Union, Optional +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights @@ -10,17 +10,19 @@ class TimespinnerLogic: flag_unchained_keys: bool flag_eye_spy: bool flag_specific_keycards: bool - pyramid_keys_unlock: Union[str, None] - present_keys_unlock: Union[str, None] - past_keys_unlock: Union[str, None] - time_keys_unlock: Union[str, None] + pyramid_keys_unlock: Optional[str] + present_keys_unlock: Optional[str] + past_keys_unlock: Optional[str] + time_keys_unlock: Optional[str] - def __init__(self, world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): + def __init__(self, player: int, options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]): self.player = player - self.flag_specific_keycards = is_option_enabled(world, player, "SpecificKeycards") - self.flag_eye_spy = is_option_enabled(world, player, "EyeSpy") - self.flag_unchained_keys = is_option_enabled(world, player, "UnchainedKeys") + self.flag_specific_keycards = bool(options and options.specific_keycards) + self.flag_eye_spy = bool(options and options.eye_spy) + self.flag_unchained_keys = bool(options and options.unchained_keys) + self.flag_prism_break = bool(options and options.prism_break) if precalculated_weights: if self.flag_unchained_keys: @@ -91,6 +93,8 @@ def can_break_walls(self, state: CollectionState) -> bool: return True def can_kill_all_3_bosses(self, state: CollectionState) -> bool: + if self.flag_prism_break: + return state.has_all({'Laser Access M', 'Laser Access I', 'Laser Access A'}, self.player) return state.has_all({'Killed Maw', 'Killed Twins', 'Killed Aelana'}, self.player) def has_teleport(self, state: CollectionState) -> bool: diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index f7921fcb81e0..72f2d8b35abf 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,59 +1,50 @@ -from typing import Dict, Union, List -from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList +from dataclasses import dataclass +from typing import Type, Any +from typing import Dict +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility, Option +from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions from schema import Schema, And, Optional, Or - class StartWithJewelryBox(Toggle): "Start with Jewelry Box unlocked" display_name = "Start with Jewelry Box" - class DownloadableItems(DefaultOnToggle): "With the tablet you will be able to download items at terminals" display_name = "Downloadable items" - class EyeSpy(Toggle): "Requires Oculus Ring in inventory to be able to break hidden walls." display_name = "Eye Spy" - class StartWithMeyef(Toggle): "Start with Meyef, ideal for when you want to play multiplayer." display_name = "Start with Meyef" - class QuickSeed(Toggle): "Start with Talaria Attachment, Nyoom!" display_name = "Quick seed" - class SpecificKeycards(Toggle): "Keycards can only open corresponding doors" display_name = "Specific Keycards" - class Inverted(Toggle): "Start in the past" display_name = "Inverted" - class GyreArchives(Toggle): "Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo" display_name = "Gyre Archives" - class Cantoran(Toggle): "Cantoran's fight and check are available upon revisiting his room" display_name = "Cantoran" - class LoreChecks(Toggle): "Memories and journal entries contain items." display_name = "Lore Checks" - class BossRando(Choice): "Wheter all boss locations are shuffled, and if their damage/hp should be scaled." display_name = "Boss Randomization" @@ -62,7 +53,6 @@ class BossRando(Choice): option_unscaled = 2 alias_true = 1 - class EnemyRando(Choice): "Wheter enemies will be randomized, and if their damage/hp should be scaled." display_name = "Enemy Randomization" @@ -72,7 +62,6 @@ class EnemyRando(Choice): option_ryshia = 3 alias_true = 1 - class DamageRando(Choice): "Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings." display_name = "Damage Rando" @@ -85,7 +74,6 @@ class DamageRando(Choice): option_manual = 6 alias_true = 2 - class DamageRandoOverrides(OptionDict): """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds""" @@ -191,14 +179,19 @@ class DamageRandoOverrides(OptionDict): "Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 }, } - class HpCap(Range): - "Sets the number that Lunais's HP maxes out at." + """Sets the number that Lunais's HP maxes out at.""" display_name = "HP Cap" range_start = 1 range_end = 999 default = 999 +class AuraCap(Range): + """Sets the maximum Aura Lunais is allowed to have. Level 1 is 80. Djinn Inferno costs 45.""" + display_name = "Aura Cap" + range_start = 45 + range_end = 999 + default = 999 class LevelCap(Range): """Sets the max level Lunais can achieve.""" @@ -207,20 +200,17 @@ class LevelCap(Range): range_end = 99 default = 99 - class ExtraEarringsXP(Range): """Adds additional XP granted by Galaxy Earrings.""" display_name = "Extra Earrings XP" range_start = 0 range_end = 24 default = 0 - class BossHealing(DefaultOnToggle): "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled." display_name = "Heal After Bosses" - class ShopFill(Choice): """Sets the items for sale in Merchant Crow's shops. Default: No sunglasses or trendy jacket, but sand vials for sale. @@ -233,12 +223,10 @@ class ShopFill(Choice): option_vanilla = 2 option_empty = 3 - class ShopWarpShards(DefaultOnToggle): "Shops always sell warp shards (when keys possessed), ignoring inventory setting." display_name = "Always Sell Warp Shards" - class ShopMultiplier(Range): "Multiplier for the cost of items in the shop. Set to 0 for free shops." display_name = "Shop Price Multiplier" @@ -246,7 +234,6 @@ class ShopMultiplier(Range): range_end = 10 default = 1 - class LootPool(Choice): """Sets the items that drop from enemies (does not apply to boss reward checks) Vanilla: Drops are the same as the base game @@ -257,7 +244,6 @@ class LootPool(Choice): option_randomized = 1 option_empty = 2 - class DropRateCategory(Choice): """Sets the drop rate when 'Loot Pool' is set to 'Random' Tiered: Based on item rarity/value @@ -271,7 +257,6 @@ class DropRateCategory(Choice): option_randomized = 2 option_fixed = 3 - class FixedDropRate(Range): "Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'" display_name = "Fixed Drop Rate" @@ -279,7 +264,6 @@ class FixedDropRate(Range): range_end = 100 default = 5 - class LootTierDistro(Choice): """Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random' Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items @@ -291,32 +275,26 @@ class LootTierDistro(Choice): option_full_random = 1 option_inverted_weight = 2 - class ShowBestiary(Toggle): "All entries in the bestiary are visible, without needing to kill one of a given enemy first" display_name = "Show Bestiary Entries" - class ShowDrops(Toggle): "All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first" display_name = "Show Bestiary Item Drops" - class EnterSandman(Toggle): "The Ancient Pyramid is unlocked by the Twin Pyramid Keys, but the final boss door opens if you have all 5 Timespinner pieces" display_name = "Enter Sandman" - class DadPercent(Toggle): """The win condition is beating the boss of Emperor's Tower""" display_name = "Dad Percent" - class RisingTides(Toggle): """Random areas are flooded or drained, can be further specified with RisingTidesOverrides""" display_name = "Rising Tides" - def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]: if with_save_point_option: return { @@ -341,7 +319,6 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D "Flooded") } - class RisingTidesOverrides(OptionDict): """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" @@ -373,13 +350,11 @@ class RisingTidesOverrides(OptionDict): "Lab": { "Dry": 67, "Flooded": 33 }, } - class UnchainedKeys(Toggle): """Start with Twin Pyramid Key, which does not give free warp; warp items for Past, Present, (and ??? with Enter Sandman) can be found.""" display_name = "Unchained Keys" - class TrapChance(Range): """Chance of traps in the item pool. Traps will only replace filler items such as potions, vials and antidotes""" @@ -388,67 +363,272 @@ class TrapChance(Range): range_end = 100 default = 10 - class Traps(OptionList): """List of traps that may be in the item pool to find""" display_name = "Traps Types" - valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" } - default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] - + valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap", "Throw Stun Trap" } + default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap", "Throw Stun Trap" ] class PresentAccessWithWheelAndSpindle(Toggle): """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" - display_name = "Past Wheel & Spindle Warp" - - -# Some options that are available in the timespinner randomizer arent currently implemented -timespinner_options: Dict[str, Option] = { - "StartWithJewelryBox": StartWithJewelryBox, - "DownloadableItems": DownloadableItems, - "EyeSpy": EyeSpy, - "StartWithMeyef": StartWithMeyef, - "QuickSeed": QuickSeed, - "SpecificKeycards": SpecificKeycards, - "Inverted": Inverted, - "GyreArchives": GyreArchives, - "Cantoran": Cantoran, - "LoreChecks": LoreChecks, - "BossRando": BossRando, - "EnemyRando": EnemyRando, - "DamageRando": DamageRando, - "DamageRandoOverrides": DamageRandoOverrides, - "HpCap": HpCap, - "LevelCap": LevelCap, - "ExtraEarringsXP": ExtraEarringsXP, - "BossHealing": BossHealing, - "ShopFill": ShopFill, - "ShopWarpShards": ShopWarpShards, - "ShopMultiplier": ShopMultiplier, - "LootPool": LootPool, - "DropRateCategory": DropRateCategory, - "FixedDropRate": FixedDropRate, - "LootTierDistro": LootTierDistro, - "ShowBestiary": ShowBestiary, - "ShowDrops": ShowDrops, - "EnterSandman": EnterSandman, - "DadPercent": DadPercent, - "RisingTides": RisingTides, - "RisingTidesOverrides": RisingTidesOverrides, - "UnchainedKeys": UnchainedKeys, - "TrapChance": TrapChance, - "Traps": Traps, - "PresentAccessWithWheelAndSpindle": PresentAccessWithWheelAndSpindle, - "DeathLink": DeathLink, -} - - -def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: - return get_option_value(world, player, name) > 0 - - -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]: - option = getattr(world, name, None) - if option == None: - return 0 - - return option[player].value + display_name = "Back to the future" + +class PrismBreak(Toggle): + """Adds 3 Laser Access items to the item pool to remove the lasers blocking the military hangar area + instead of needing to beat the Golden Idol, Aelana, and The Maw.""" + display_name = "Prism Break" + +@dataclass +class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): + start_with_jewelry_box: StartWithJewelryBox + downloadable_items: DownloadableItems + eye_spy: EyeSpy + start_with_meyef: StartWithMeyef + quick_seed: QuickSeed + specific_keycards: SpecificKeycards + inverted: Inverted + gyre_archives: GyreArchives + cantoran: Cantoran + lore_checks: LoreChecks + boss_rando: BossRando + enemy_rando: EnemyRando + damage_rando: DamageRando + damage_rando_overrides: DamageRandoOverrides + hp_cap: HpCap + aura_cap: AuraCap + level_cap: LevelCap + extra_earrings_xp: ExtraEarringsXP + boss_healing: BossHealing + shop_fill: ShopFill + shop_warp_shards: ShopWarpShards + shop_multiplier: ShopMultiplier + loot_pool: LootPool + drop_rate_category: DropRateCategory + fixed_drop_rate: FixedDropRate + loot_tier_distro: LootTierDistro + show_bestiary: ShowBestiary + show_drops: ShowDrops + enter_sandman: EnterSandman + dad_percent: DadPercent + rising_tides: RisingTides + rising_tides_overrides: RisingTidesOverrides + unchained_keys: UnchainedKeys + back_to_the_future: PresentAccessWithWheelAndSpindle + prism_break: PrismBreak + trap_chance: TrapChance + traps: Traps + +class HiddenDamageRandoOverrides(DamageRandoOverrides): + """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that + you don't specify will roll with 1/1/1 as odds""" + visibility = Visibility.none + +class HiddenRisingTidesOverrides(RisingTidesOverrides): + """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. + Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" + visibility = Visibility.none + +class HiddenTraps(Traps): + """List of traps that may be in the item pool to find""" + visibility = Visibility.none + +class HiddenDeathLink(DeathLink): + """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" + visibility = Visibility.none + +def hidden(option: Type[Option[Any]]) -> Type[Option]: + new_option = AssembleOptions(f"{option.__name__}Hidden", option.__bases__, vars(option).copy()) + new_option.visibility = Visibility.none + new_option.__doc__ = option.__doc__ + globals()[f"{option.__name__}Hidden"] = new_option + return new_option + +class HasReplacedCamelCase(Toggle): + """For internal use will display a warning message if true""" + visibility = Visibility.none + +@dataclass +class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): + StartWithJewelryBox: hidden(StartWithJewelryBox) # type: ignore + DownloadableItems: hidden(DownloadableItems) # type: ignore + EyeSpy: hidden(EyeSpy) # type: ignore + StartWithMeyef: hidden(StartWithMeyef) # type: ignore + QuickSeed: hidden(QuickSeed) # type: ignore + SpecificKeycards: hidden(SpecificKeycards) # type: ignore + Inverted: hidden(Inverted) # type: ignore + GyreArchives: hidden(GyreArchives) # type: ignore + Cantoran: hidden(Cantoran) # type: ignore + LoreChecks: hidden(LoreChecks) # type: ignore + BossRando: hidden(BossRando) # type: ignore + EnemyRando: hidden(EnemyRando) # type: ignore + DamageRando: hidden(DamageRando) # type: ignore + DamageRandoOverrides: HiddenDamageRandoOverrides + HpCap: hidden(HpCap) # type: ignore + LevelCap: hidden(LevelCap) # type: ignore + ExtraEarringsXP: hidden(ExtraEarringsXP) # type: ignore + BossHealing: hidden(BossHealing) # type: ignore + ShopFill: hidden(ShopFill) # type: ignore + ShopWarpShards: hidden(ShopWarpShards) # type: ignore + ShopMultiplier: hidden(ShopMultiplier) # type: ignore + LootPool: hidden(LootPool) # type: ignore + DropRateCategory: hidden(DropRateCategory) # type: ignore + FixedDropRate: hidden(FixedDropRate) # type: ignore + LootTierDistro: hidden(LootTierDistro) # type: ignore + ShowBestiary: hidden(ShowBestiary) # type: ignore + ShowDrops: hidden(ShowDrops) # type: ignore + EnterSandman: hidden(EnterSandman) # type: ignore + DadPercent: hidden(DadPercent) # type: ignore + RisingTides: hidden(RisingTides) # type: ignore + RisingTidesOverrides: HiddenRisingTidesOverrides + UnchainedKeys: hidden(UnchainedKeys) # type: ignore + PresentAccessWithWheelAndSpindle: hidden(PresentAccessWithWheelAndSpindle) # type: ignore + TrapChance: hidden(TrapChance) # type: ignore + Traps: HiddenTraps # type: ignore + DeathLink: HiddenDeathLink # type: ignore + has_replaced_options: HasReplacedCamelCase + + def handle_backward_compatibility(self) -> None: + if self.StartWithJewelryBox != StartWithJewelryBox.default and \ + self.start_with_jewelry_box == StartWithJewelryBox.default: + self.start_with_jewelry_box.value = self.StartWithJewelryBox.value + self.has_replaced_options.value = Toggle.option_true + if self.DownloadableItems != DownloadableItems.default and \ + self.downloadable_items == DownloadableItems.default: + self.downloadable_items.value = self.DownloadableItems.value + self.has_replaced_options.value = Toggle.option_true + if self.EyeSpy != EyeSpy.default and \ + self.eye_spy == EyeSpy.default: + self.eye_spy.value = self.EyeSpy.value + self.has_replaced_options.value = Toggle.option_true + if self.StartWithMeyef != StartWithMeyef.default and \ + self.start_with_meyef == StartWithMeyef.default: + self.start_with_meyef.value = self.StartWithMeyef.value + self.has_replaced_options.value = Toggle.option_true + if self.QuickSeed != QuickSeed.default and \ + self.quick_seed == QuickSeed.default: + self.quick_seed.value = self.QuickSeed.value + self.has_replaced_options.value = Toggle.option_true + if self.SpecificKeycards != SpecificKeycards.default and \ + self.specific_keycards == SpecificKeycards.default: + self.specific_keycards.value = self.SpecificKeycards.value + self.has_replaced_options.value = Toggle.option_true + if self.Inverted != Inverted.default and \ + self.inverted == Inverted.default: + self.inverted.value = self.Inverted.value + self.has_replaced_options.value = Toggle.option_true + if self.GyreArchives != GyreArchives.default and \ + self.gyre_archives == GyreArchives.default: + self.gyre_archives.value = self.GyreArchives.value + self.has_replaced_options.value = Toggle.option_true + if self.Cantoran != Cantoran.default and \ + self.cantoran == Cantoran.default: + self.cantoran.value = self.Cantoran.value + self.has_replaced_options.value = Toggle.option_true + if self.LoreChecks != LoreChecks.default and \ + self.lore_checks == LoreChecks.default: + self.lore_checks.value = self.LoreChecks.value + self.has_replaced_options.value = Toggle.option_true + if self.BossRando != BossRando.default and \ + self.boss_rando == BossRando.default: + self.boss_rando.value = self.BossRando.value + self.has_replaced_options.value = Toggle.option_true + if self.EnemyRando != EnemyRando.default and \ + self.enemy_rando == EnemyRando.default: + self.enemy_rando.value = self.EnemyRando.value + self.has_replaced_options.value = Toggle.option_true + if self.DamageRando != DamageRando.default and \ + self.damage_rando == DamageRando.default: + self.damage_rando.value = self.DamageRando.value + self.has_replaced_options.value = Toggle.option_true + if self.DamageRandoOverrides != DamageRandoOverrides.default and \ + self.damage_rando_overrides == DamageRandoOverrides.default: + self.damage_rando_overrides.value = self.DamageRandoOverrides.value + self.has_replaced_options.value = Toggle.option_true + if self.HpCap != HpCap.default and \ + self.hp_cap == HpCap.default: + self.hp_cap.value = self.HpCap.value + self.has_replaced_options.value = Toggle.option_true + if self.LevelCap != LevelCap.default and \ + self.level_cap == LevelCap.default: + self.level_cap.value = self.LevelCap.value + self.has_replaced_options.value = Toggle.option_true + if self.ExtraEarringsXP != ExtraEarringsXP.default and \ + self.extra_earrings_xp == ExtraEarringsXP.default: + self.extra_earrings_xp.value = self.ExtraEarringsXP.value + self.has_replaced_options.value = Toggle.option_true + if self.BossHealing != BossHealing.default and \ + self.boss_healing == BossHealing.default: + self.boss_healing.value = self.BossHealing.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopFill != ShopFill.default and \ + self.shop_fill == ShopFill.default: + self.shop_fill.value = self.ShopFill.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopWarpShards != ShopWarpShards.default and \ + self.shop_warp_shards == ShopWarpShards.default: + self.shop_warp_shards.value = self.ShopWarpShards.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopMultiplier != ShopMultiplier.default and \ + self.shop_multiplier == ShopMultiplier.default: + self.shop_multiplier.value = self.ShopMultiplier.value + self.has_replaced_options.value = Toggle.option_true + if self.LootPool != LootPool.default and \ + self.loot_pool == LootPool.default: + self.loot_pool.value = self.LootPool.value + self.has_replaced_options.value = Toggle.option_true + if self.DropRateCategory != DropRateCategory.default and \ + self.drop_rate_category == DropRateCategory.default: + self.drop_rate_category.value = self.DropRateCategory.value + self.has_replaced_options.value = Toggle.option_true + if self.FixedDropRate != FixedDropRate.default and \ + self.fixed_drop_rate == FixedDropRate.default: + self.fixed_drop_rate.value = self.FixedDropRate.value + self.has_replaced_options.value = Toggle.option_true + if self.LootTierDistro != LootTierDistro.default and \ + self.loot_tier_distro == LootTierDistro.default: + self.loot_tier_distro.value = self.LootTierDistro.value + self.has_replaced_options.value = Toggle.option_true + if self.ShowBestiary != ShowBestiary.default and \ + self.show_bestiary == ShowBestiary.default: + self.show_bestiary.value = self.ShowBestiary.value + self.has_replaced_options.value = Toggle.option_true + if self.ShowDrops != ShowDrops.default and \ + self.show_drops == ShowDrops.default: + self.show_drops.value = self.ShowDrops.value + self.has_replaced_options.value = Toggle.option_true + if self.EnterSandman != EnterSandman.default and \ + self.enter_sandman == EnterSandman.default: + self.enter_sandman.value = self.EnterSandman.value + self.has_replaced_options.value = Toggle.option_true + if self.DadPercent != DadPercent.default and \ + self.dad_percent == DadPercent.default: + self.dad_percent.value = self.DadPercent.value + self.has_replaced_options.value = Toggle.option_true + if self.RisingTides != RisingTides.default and \ + self.rising_tides == RisingTides.default: + self.rising_tides.value = self.RisingTides.value + self.has_replaced_options.value = Toggle.option_true + if self.RisingTidesOverrides != RisingTidesOverrides.default and \ + self.rising_tides_overrides == RisingTidesOverrides.default: + self.rising_tides_overrides.value = self.RisingTidesOverrides.value + self.has_replaced_options.value = Toggle.option_true + if self.UnchainedKeys != UnchainedKeys.default and \ + self.unchained_keys == UnchainedKeys.default: + self.unchained_keys.value = self.UnchainedKeys.value + self.has_replaced_options.value = Toggle.option_true + if self.PresentAccessWithWheelAndSpindle != PresentAccessWithWheelAndSpindle.default and \ + self.back_to_the_future == PresentAccessWithWheelAndSpindle.default: + self.back_to_the_future.value = self.PresentAccessWithWheelAndSpindle.value + self.has_replaced_options.value = Toggle.option_true + if self.TrapChance != TrapChance.default and \ + self.trap_chance == TrapChance.default: + self.trap_chance.value = self.TrapChance.value + self.has_replaced_options.value = Toggle.option_true + if self.Traps != Traps.default and \ + self.traps == Traps.default: + self.traps.value = self.Traps.value + self.has_replaced_options.value = Toggle.option_true + if self.DeathLink != DeathLink.default and \ + self.death_link == DeathLink.default: + self.death_link.value = self.DeathLink.value + self.has_replaced_options.value = Toggle.option_true diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index ff7f031d3b67..c9d80d7a709d 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,6 +1,6 @@ from typing import Tuple, Dict, Union, List -from BaseClasses import MultiWorld -from .Options import timespinner_options, is_option_enabled, get_option_value +from random import Random +from .Options import TimespinnerOptions class PreCalculatedWeights: pyramid_keys_unlock: str @@ -21,22 +21,22 @@ class PreCalculatedWeights: flood_lake_serene_bridge: bool flood_lab: bool - def __init__(self, world: MultiWorld, player: int): - if world and is_option_enabled(world, player, "RisingTides"): - weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) + def __init__(self, options: TimespinnerOptions, random: Random): + if options.rising_tides: + weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(options) self.flood_basement, self.flood_basement_high = \ - self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") - self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") - self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") - self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") - self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") - self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") - self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") - self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - self.flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") - self.flood_lake_serene_bridge, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSereneBridge") - self.flood_lab, _ = self.roll_flood_setting(world, player, weights_overrrides, "Lab") + self.roll_flood_setting(random, weights_overrrides, "CastleBasement") + self.flood_xarion, _ = self.roll_flood_setting(random, weights_overrrides, "Xarion") + self.flood_maw, _ = self.roll_flood_setting(random, weights_overrrides, "Maw") + self.flood_pyramid_shaft, _ = self.roll_flood_setting(random, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back, _ = self.roll_flood_setting(random, weights_overrrides, "Sandman") + self.flood_moat, _ = self.roll_flood_setting(random, weights_overrrides, "CastleMoat") + self.flood_courtyard, _ = self.roll_flood_setting(random, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation, _ = self.roll_flood_setting(random, weights_overrrides, "LakeDesolation") + self.flood_lake_serene, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSerene") + self.flood_lake_serene_bridge, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSereneBridge") + self.flood_lab, _ = self.roll_flood_setting(random, weights_overrrides, "Lab") else: self.flood_basement = False self.flood_basement_high = False @@ -52,10 +52,12 @@ def __init__(self, world: MultiWorld, player: int): self.flood_lab = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlocks(world, player, self.flood_maw, self.flood_xarion) + self.get_pyramid_keys_unlocks(options, random, self.flood_maw, self.flood_xarion) @staticmethod - def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + def get_pyramid_keys_unlocks(options: TimespinnerOptions, random: Random, + is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + present_teleportation_gates: List[str] = [ "GateKittyBoss", "GateLeftLibrary", @@ -80,38 +82,30 @@ def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: boo "GateRightPyramid" ) - if not world: - return ( - present_teleportation_gates[0], - present_teleportation_gates[0], - past_teleportation_gates[0], - ancient_pyramid_teleportation_gates[0] - ) - if not is_maw_flooded: past_teleportation_gates.append("GateMaw") if not is_xarion_flooded: present_teleportation_gates.append("GateXarion") - if is_option_enabled(world, player, "Inverted"): + if options.inverted: all_gates: Tuple[str, ...] = present_teleportation_gates else: all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates return ( - world.random.choice(all_gates), - world.random.choice(present_teleportation_gates), - world.random.choice(past_teleportation_gates), - world.random.choice(ancient_pyramid_teleportation_gates) + random.choice(all_gates), + random.choice(present_teleportation_gates), + random.choice(past_teleportation_gates), + random.choice(ancient_pyramid_teleportation_gates) ) @staticmethod - def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: + def get_flood_weights_overrides(options: TimespinnerOptions) -> Dict[str, Union[str, Dict[str, int]]]: weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ - get_option_value(world, player, "RisingTidesOverrides") + options.rising_tides_overrides.value - default_weights: Dict[str, Dict[str, int]] = timespinner_options["RisingTidesOverrides"].default + default_weights: Dict[str, Dict[str, int]] = options.rising_tides_overrides.default if not weights_overrides_option: weights_overrides_option = default_weights @@ -123,13 +117,13 @@ def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Uni return weights_overrides_option @staticmethod - def roll_flood_setting(world: MultiWorld, player: int, - all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: + def roll_flood_setting(random: Random, all_weights: Dict[str, Union[Dict[str, int], str]], + key: str) -> Tuple[bool, bool]: weights: Union[Dict[str, int], str] = all_weights[key] if isinstance(weights, dict): - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + result: str = random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] else: result: str = weights diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 757a41c38821..f737b461d0bc 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,14 +1,16 @@ from typing import List, Set, Dict, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location -from .Options import is_option_enabled +from .Options import TimespinnerOptions from .Locations import LocationData, get_location_datas from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic -def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): +def create_regions_and_locations(world: MultiWorld, player: int, options: TimespinnerOptions, + precalculated_weights: PreCalculatedWeights): + locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region( - get_location_datas(world, player, precalculated_weights)) + get_location_datas(player, options, precalculated_weights)) regions = [ create_region(world, player, locations_per_region, 'Menu'), @@ -53,7 +55,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w create_region(world, player, locations_per_region, 'Space time continuum') ] - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: regions.extend([ create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'), create_region(world, player, locations_per_region, 'Ifrit\'s Lair'), @@ -64,10 +66,10 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w world.regions += regions - connectStartingRegion(world, player) + connectStartingRegion(world, player, options) flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) + logic = TimespinnerLogic(player, options, precalculated_weights) connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") @@ -123,7 +125,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft') connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Refugee Camp', 'Forest') - connect(world, player, 'Refugee Camp', 'Library', lambda state: is_option_enabled(world, player, "Inverted") and is_option_enabled(world, player, "PresentAccessWithWheelAndSpindle") and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) + connect(world, player, 'Refugee Camp', 'Library', lambda state: options.inverted and options.back_to_the_future and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) connect(world, player, 'Forest', 'Refugee Camp') connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state)) @@ -178,11 +180,11 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) connect(world, player, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) - connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not options.unchained_keys and options.enter_sandman)) connect(world, player, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) connect(world, player, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp") @@ -220,12 +222,12 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str return region -def connectStartingRegion(world: MultiWorld, player: int): +def connectStartingRegion(world: MultiWorld, player: int, options: TimespinnerOptions): menu = world.get_region('Menu', player) tutorial = world.get_region('Tutorial', player) space_time_continuum = world.get_region('Space time continuum', player) - if is_option_enabled(world, player, "Inverted"): + if options.inverted: starting_region = world.get_region('Refugee Camp', player) else: starting_region = world.get_region('Lake desolation', player) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cab6fb648b95..ca31d08326b5 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,12 +1,13 @@ -from typing import Dict, List, Set, Tuple, TextIO, Union -from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from typing import Dict, List, Set, Tuple, TextIO, Any, Optional +from BaseClasses import Item, Tutorial, ItemClassification from .Items import get_item_names_per_category from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items from .Locations import get_location_datas, EventId -from .Options import is_option_enabled, get_option_value, timespinner_options +from .Options import BackwardsCompatiableTimespinnerOptions, Toggle from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld +import logging class TimespinnerWebWorld(WebWorld): theme = "ice" @@ -35,32 +36,39 @@ class TimespinnerWorld(World): Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers. Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family. """ - - option_definitions = timespinner_options + options_dataclass = BackwardsCompatiableTimespinnerOptions + options: BackwardsCompatiableTimespinnerOptions game = "Timespinner" topology_present = True web = TimespinnerWebWorld() required_client_version = (0, 4, 2) item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} + location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)} item_name_groups = get_item_names_per_category() precalculated_weights: PreCalculatedWeights def generate_early(self) -> None: - self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player) + self.options.handle_backward_compatibility() + + self.precalculated_weights = PreCalculatedWeights(self.options, self.random) # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly - if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: - self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true - if self.multiworld.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0: - self.multiworld.QuickSeed[self.player].value = self.multiworld.QuickSeed[self.player].option_true - if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0: - self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true + if self.options.start_inventory.value.pop("Meyef", 0) > 0: + self.options.start_with_meyef.value = Toggle.option_true + if self.options.start_inventory.value.pop("Talaria Attachment", 0) > 0: + self.options.quick_seed.value = Toggle.option_true + if self.options.start_inventory.value.pop("Jewelry Box", 0) > 0: + self.options.start_with_jewelry_box.value = Toggle.option_true + + self.interpret_slot_data(None) + + if self.options.quick_seed: + self.multiworld.push_precollected(self.create_item("Talaria Attachment")) def create_regions(self) -> None: - create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights) + create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights) def create_items(self) -> None: self.create_and_assign_event_items() @@ -74,7 +82,7 @@ def create_items(self) -> None: def set_rules(self) -> None: final_boss: str - if self.is_option_enabled("DadPercent"): + if self.options.dad_percent: final_boss = "Killed Emperor" else: final_boss = "Killed Nightmare" @@ -82,48 +90,147 @@ def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player) def fill_slot_data(self) -> Dict[str, object]: - slot_data: Dict[str, object] = {} - - ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"} - - for option_name in timespinner_options: - if (option_name not in ap_specific_settings): - slot_data[option_name] = self.get_option_value(option_name) - - slot_data["StinkyMaw"] = True - slot_data["ProgressiveVerticalMovement"] = False - slot_data["ProgressiveKeycards"] = False - slot_data["PersonalItems"] = self.get_personal_items() - slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock - slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock - slot_data["PastGate"] = self.precalculated_weights.past_key_unlock - slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock - slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \ - int(self.precalculated_weights.flood_basement_high) - slot_data["Xarion"] = self.precalculated_weights.flood_xarion - slot_data["Maw"] = self.precalculated_weights.flood_maw - slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft - slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back - slot_data["CastleMoat"] = self.precalculated_weights.flood_moat - slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard - slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation - slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene - slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge - slot_data["Lab"] = self.precalculated_weights.flood_lab + return { + # options + "StartWithJewelryBox": self.options.start_with_jewelry_box.value, + "DownloadableItems": self.options.downloadable_items.value, + "EyeSpy": self.options.eye_spy.value, + "StartWithMeyef": self.options.start_with_meyef.value, + "QuickSeed": self.options.quick_seed.value, + "SpecificKeycards": self.options.specific_keycards.value, + "Inverted": self.options.inverted.value, + "GyreArchives": self.options.gyre_archives.value, + "Cantoran": self.options.cantoran.value, + "LoreChecks": self.options.lore_checks.value, + "BossRando": self.options.boss_rando.value, + "EnemyRando": self.options.enemy_rando.value, + "DamageRando": self.options.damage_rando.value, + "DamageRandoOverrides": self.options.damage_rando_overrides.value, + "HpCap": self.options.hp_cap.value, + "AuraCap": self.options.aura_cap.value, + "LevelCap": self.options.level_cap.value, + "ExtraEarringsXP": self.options.extra_earrings_xp.value, + "BossHealing": self.options.boss_healing.value, + "ShopFill": self.options.shop_fill.value, + "ShopWarpShards": self.options.shop_warp_shards.value, + "ShopMultiplier": self.options.shop_multiplier.value, + "LootPool": self.options.loot_pool.value, + "DropRateCategory": self.options.drop_rate_category.value, + "FixedDropRate": self.options.fixed_drop_rate.value, + "LootTierDistro": self.options.loot_tier_distro.value, + "ShowBestiary": self.options.show_bestiary.value, + "ShowDrops": self.options.show_drops.value, + "EnterSandman": self.options.enter_sandman.value, + "DadPercent": self.options.dad_percent.value, + "RisingTides": self.options.rising_tides.value, + "UnchainedKeys": self.options.unchained_keys.value, + "PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value, + "PrismBreak": self.options.prism_break.value, + "Traps": self.options.traps.value, + "DeathLink": self.options.death_link.value, + "StinkyMaw": True, + # data + "PersonalItems": self.get_personal_items(), + "PyramidKeysGate": self.precalculated_weights.pyramid_keys_unlock, + "PresentGate": self.precalculated_weights.present_key_unlock, + "PastGate": self.precalculated_weights.past_key_unlock, + "TimeGate": self.precalculated_weights.time_key_unlock, + # rising tides + "Basement": int(self.precalculated_weights.flood_basement) + \ + int(self.precalculated_weights.flood_basement_high), + "Xarion": self.precalculated_weights.flood_xarion, + "Maw": self.precalculated_weights.flood_maw, + "PyramidShaft": self.precalculated_weights.flood_pyramid_shaft, + "BackPyramid": self.precalculated_weights.flood_pyramid_back, + "CastleMoat": self.precalculated_weights.flood_moat, + "CastleCourtyard": self.precalculated_weights.flood_courtyard, + "LakeDesolation": self.precalculated_weights.flood_lake_desolation, + "DryLakeSerene": not self.precalculated_weights.flood_lake_serene, + "LakeSereneBridge": self.precalculated_weights.flood_lake_serene_bridge, + "Lab": self.precalculated_weights.flood_lab + } + + def interpret_slot_data(self, slot_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """Used by Universal Tracker to correctly rebuild state""" + + if not slot_data \ + and hasattr(self.multiworld, "re_gen_passthrough") \ + and isinstance(self.multiworld.re_gen_passthrough, dict) \ + and "Timespinner" in self.multiworld.re_gen_passthrough: + slot_data = self.multiworld.re_gen_passthrough["Timespinner"] + + if not slot_data: + return None + + self.options.start_with_jewelry_box.value = slot_data["StartWithJewelryBox"] + self.options.downloadable_items.value = slot_data["DownloadableItems"] + self.options.eye_spy.value = slot_data["EyeSpy"] + self.options.start_with_meyef.value = slot_data["StartWithMeyef"] + self.options.quick_seed.value = slot_data["QuickSeed"] + self.options.specific_keycards.value = slot_data["SpecificKeycards"] + self.options.inverted.value = slot_data["Inverted"] + self.options.gyre_archives.value = slot_data["GyreArchives"] + self.options.cantoran.value = slot_data["Cantoran"] + self.options.lore_checks.value = slot_data["LoreChecks"] + self.options.boss_rando.value = slot_data["BossRando"] + self.options.damage_rando.value = slot_data["DamageRando"] + self.options.damage_rando_overrides.value = slot_data["DamageRandoOverrides"] + self.options.hp_cap.value = slot_data["HpCap"] + self.options.level_cap.value = slot_data["LevelCap"] + self.options.extra_earrings_xp.value = slot_data["ExtraEarringsXP"] + self.options.boss_healing.value = slot_data["BossHealing"] + self.options.shop_fill.value = slot_data["ShopFill"] + self.options.shop_warp_shards.value = slot_data["ShopWarpShards"] + self.options.shop_multiplier.value = slot_data["ShopMultiplier"] + self.options.loot_pool.value = slot_data["LootPool"] + self.options.drop_rate_category.value = slot_data["DropRateCategory"] + self.options.fixed_drop_rate.value = slot_data["FixedDropRate"] + self.options.loot_tier_distro.value = slot_data["LootTierDistro"] + self.options.show_bestiary.value = slot_data["ShowBestiary"] + self.options.show_drops.value = slot_data["ShowDrops"] + self.options.enter_sandman.value = slot_data["EnterSandman"] + self.options.dad_percent.value = slot_data["DadPercent"] + self.options.rising_tides.value = slot_data["RisingTides"] + self.options.unchained_keys.value = slot_data["UnchainedKeys"] + self.options.back_to_the_future.value = slot_data["PresentAccessWithWheelAndSpindle"] + self.options.traps.value = slot_data["Traps"] + self.options.death_link.value = slot_data["DeathLink"] + # Readonly slot_data["StinkyMaw"] + # data + # Readonly slot_data["PersonalItems"] + self.precalculated_weights.pyramid_keys_unlock = slot_data["PyramidKeysGate"] + self.precalculated_weights.present_key_unlock = slot_data["PresentGate"] + self.precalculated_weights.past_key_unlock = slot_data["PastGate"] + self.precalculated_weights.time_key_unlock = slot_data["TimeGate"] + # rising tides + if (slot_data["Basement"] > 1): + self.precalculated_weights.flood_basement = True + if (slot_data["Basement"] == 2): + self.precalculated_weights.flood_basement_high = True + self.precalculated_weights.flood_xarion = slot_data["Xarion"] + self.precalculated_weights.flood_maw = slot_data["Maw"] + self.precalculated_weights.flood_pyramid_shaft = slot_data["PyramidShaft"] + self.precalculated_weights.flood_pyramid_back = slot_data["BackPyramid"] + self.precalculated_weights.flood_moat = slot_data["CastleMoat"] + self.precalculated_weights.flood_courtyard = slot_data["CastleCourtyard"] + self.precalculated_weights.flood_lake_desolation = slot_data["LakeDesolation"] + self.precalculated_weights.flood_lake_serene = not slot_data["DryLakeSerene"] + self.precalculated_weights.flood_lake_serene_bridge = slot_data["LakeSereneBridge"] + self.precalculated_weights.flood_lab = slot_data["Lab"] return slot_data def write_spoiler_header(self, spoiler_handle: TextIO) -> None: - if self.is_option_enabled("UnchainedKeys"): + if self.options.unchained_keys: spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n') spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n') - if self.is_option_enabled("EnterSandman"): + if self.options.enter_sandman: spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n') else: spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n') - if self.is_option_enabled("RisingTides"): + if self.options.rising_tides: flooded_areas: List[str] = [] if self.precalculated_weights.flood_basement: @@ -159,6 +266,15 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n') + if self.options.has_replaced_options: + warning = \ + f"NOTICE: Timespinner options for player '{self.player_name}' were renamed from PascalCase to snake_case, " \ + "please update your yaml" + + spoiler_handle.write("\n") + spoiler_handle.write(warning) + logging.warning(warning) + def create_item(self, name: str) -> Item: data = item_table[name] @@ -176,47 +292,55 @@ def create_item(self, name: str) -> Item: if not item.advancement: return item - if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"): + if (name == 'Tablet' or name == 'Library Keycard V') and not self.options.downloadable_items: item.classification = ItemClassification.filler - elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"): + elif name == 'Oculus Ring' and not self.options.eye_spy: item.classification = ItemClassification.filler - elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"): + elif (name == 'Kobo' or name == 'Merchant Crow') and not self.options.gyre_archives: item.classification = ItemClassification.filler elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ - and not self.is_option_enabled("UnchainedKeys"): + and not self.options.unchained_keys: + item.classification = ItemClassification.filler + elif name in {"Laser Access A", "Laser Access I", "Laser Access M"} \ + and not self.options.prism_break: item.classification = ItemClassification.filler return item def get_filler_item_name(self) -> str: - trap_chance: int = self.get_option_value("TrapChance") - enabled_traps: List[str] = self.get_option_value("Traps") + trap_chance: int = self.options.trap_chance.value + enabled_traps: List[str] = self.options.traps.value - if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: - return self.multiworld.random.choice(enabled_traps) + if self.random.random() < (trap_chance / 100) and enabled_traps: + return self.random.choice(enabled_traps) else: - return self.multiworld.random.choice(filler_items) + return self.random.choice(filler_items) def get_excluded_items(self) -> Set[str]: excluded_items: Set[str] = set() - if self.is_option_enabled("StartWithJewelryBox"): + if self.options.start_with_jewelry_box: excluded_items.add('Jewelry Box') - if self.is_option_enabled("StartWithMeyef"): + if self.options.start_with_meyef: excluded_items.add('Meyef') - if self.is_option_enabled("QuickSeed"): + if self.options.quick_seed: excluded_items.add('Talaria Attachment') - if self.is_option_enabled("UnchainedKeys"): + if self.options.unchained_keys: excluded_items.add('Twin Pyramid Key') - if not self.is_option_enabled("EnterSandman"): + if not self.options.enter_sandman: excluded_items.add('Mysterious Warp Beacon') else: excluded_items.add('Timeworn Warp Beacon') excluded_items.add('Modern Warp Beacon') excluded_items.add('Mysterious Warp Beacon') + if not self.options.prism_break: + excluded_items.add('Laser Access A') + excluded_items.add('Laser Access I') + excluded_items.add('Laser Access M') + for item in self.multiworld.precollected_items[self.player]: if item.name not in self.item_name_groups['UseItem']: excluded_items.add(item.name) @@ -224,8 +348,8 @@ def get_excluded_items(self) -> Set[str]: return excluded_items def assign_starter_items(self, excluded_items: Set[str]) -> None: - non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value - local_items: Set[str] = self.multiworld.local_items[self.player].value + non_local_items: Set[str] = self.options.non_local_items.value + local_items: Set[str] = self.options.local_items.value local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item in local_items or not item in non_local_items) @@ -247,27 +371,26 @@ def assign_starter_items(self, excluded_items: Set[str]) -> None: self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells) def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None: - item_name = self.multiworld.random.choice(item_list) + item_name = self.random.choice(item_list) self.place_locked_item(excluded_items, location, item_name) def place_first_progression_item(self, excluded_items: Set[str]) -> None: - if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \ - or self.precalculated_weights.flood_lake_desolation: + if self.options.quick_seed or self.options.inverted or self.precalculated_weights.flood_lake_desolation: return - for item in self.multiworld.precollected_items[self.player]: - if item.name in starter_progression_items and not item.name in excluded_items: + for item_name in self.options.start_inventory.value.keys(): + if item_name in starter_progression_items: return local_starter_progression_items = tuple( item for item in starter_progression_items - if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value) + if item not in excluded_items and item not in self.options.non_local_items.value) if not local_starter_progression_items: return - progression_item = self.multiworld.random.choice(local_starter_progression_items) + progression_item = self.random.choice(local_starter_progression_items) self.multiworld.local_early_items[self.player][progression_item] = 1 @@ -307,9 +430,3 @@ def get_personal_items(self) -> Dict[int, int]: personal_items[location.address] = location.item.code return personal_items - - def is_option_enabled(self, option: str) -> bool: - return is_option_enabled(self.multiworld, self.player, option) - - def get_option_value(self, option: str) -> Union[int, Dict, List]: - return get_option_value(self.multiworld, self.player, option) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 5b90e99722df..7c57dc5a4b26 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -1,3 +1,5 @@ +from collections import Counter + from BaseClasses import ItemClassification from .Locations import level_locations, all_level_locations, standard_level_locations, shop_locations from .Options import TriforceLocations, StartingPosition @@ -58,11 +60,11 @@ "Small Key": 2, "Five Rupees": 2 } -basic_pool = { - item: overworld_items.get(item, 0) + shop_items.get(item, 0) - + major_dungeon_items.get(item, 0) + map_compass_replacements.get(item, 0) - for item in set(overworld_items) | set(shop_items) | set(major_dungeon_items) | set(map_compass_replacements) -} +basic_pool = Counter() +basic_pool.update(overworld_items) +basic_pool.update(shop_items) +basic_pool.update(major_dungeon_items) +basic_pool.update(map_compass_replacements) starting_weapons = ["Sword", "White Sword", "Magical Sword", "Magical Rod", "Red Candle"] guaranteed_shop_items = ["Small Key", "Bomb", "Water of Life (Red)", "Arrow"] @@ -80,7 +82,7 @@ def generate_itempool(tlozworld): location.item.classification = ItemClassification.progression def get_pool_core(world): - random = world.multiworld.random + random = world.random pool = [] placed_items = {} @@ -132,21 +134,13 @@ def get_pool_core(world): else: pool.append(fragment) - # Level 9 junk fill - if world.options.ExpandedPool > 0: - spots = random.sample(level_locations[8], len(level_locations[8]) // 2) - for spot in spots: - junk = random.choice(list(minor_items.keys())) - placed_items[spot] = junk - minor_items[junk] -= 1 - # Finish Pool final_pool = basic_pool if world.options.ExpandedPool: - final_pool = { - item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0) - for item in set(basic_pool) | set(minor_items) | set(take_any_items) - } + final_pool = Counter() + final_pool.update(basic_pool) + final_pool.update(minor_items) + final_pool.update(take_any_items) final_pool["Five Rupees"] -= 1 for item in final_pool.keys(): for i in range(0, final_pool[item]): diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py index 5b30357c940c..f95e5d80443e 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -99,12 +99,24 @@ "Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right" ] +take_any_locations = [ + "Take Any Item Left", "Take Any Item Middle", "Take Any Item Right" +] + +sword_cave_locations = [ + "Starting Sword Cave", "White Sword Pond", "Magical Sword Grave" +] + food_locations = [ - "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", + "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" ] +gohma_locations = [ + "Level 6 Boss", "Level 6 Triforce", "Level 8 Item (Magical Key)", "Level 8 Bomb Drop (Darknuts North)" +] + gleeok_locations = [ "Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce" ] diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 39c3b954f0d4..de627a533bd3 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from worlds.generic.Rules import add_rule -from .Locations import food_locations, shop_locations, gleeok_locations +from .Locations import food_locations, shop_locations, gleeok_locations, gohma_locations from .ItemPool import dangerous_weapon_locations from .Options import StartingPosition @@ -10,13 +10,12 @@ def set_rules(tloz_world: "TLoZWorld"): player = tloz_world.player - world = tloz_world.multiworld options = tloz_world.options # Boss events for a nicer spoiler log play through for level in range(1, 9): - boss = world.get_location(f"Level {level} Boss", player) - boss_event = world.get_location(f"Level {level} Boss Status", player) + boss = tloz_world.get_location(f"Level {level} Boss") + boss_event = tloz_world.get_location(f"Level {level} Boss Status") status = tloz_world.create_event(f"Boss {level} Defeated") boss_event.place_locked_item(status) add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) @@ -26,136 +25,131 @@ def set_rules(tloz_world: "TLoZWorld"): for location in level.locations: if options.StartingPosition < StartingPosition.option_dangerous \ or location.name not in dangerous_weapon_locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("weapons", player)) # This part of the loop sets up an expected amount of defense needed for each dungeon if i > 0: # Don't need an extra heart for Level 1 - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state, hearts=i: state.has("Heart Container", player, hearts) or (state.has("Blue Ring", player) and state.has("Heart Container", player, int(hearts / 2))) or (state.has("Red Ring", player) and state.has("Heart Container", player, int(hearts / 4)))) if "Pols Voice" in location.name: # This enemy needs specific weapons - add_rule(world.get_location(location.name, player), - lambda state: state.has_group("swords", player) or state.has("Bow", player)) + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_group("swords", player) or + (state.has("Bow", player) and state.has_group("arrows", player))) # No requiring anything in a shop until we can farm for money for location in shop_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("weapons", player)) # Everything from 4 on up has dark rooms for level in tloz_world.levels[4:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player) or (state.has("Magical Rod", player) and state.has("Book of Magic", player))) # Everything from 5 on up has gaps for level in tloz_world.levels[5:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Stepladder", player)) - add_rule(world.get_location("Level 5 Boss", player), - lambda state: state.has("Recorder", player)) - - add_rule(world.get_location("Level 6 Boss", player), - lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + # Level 4 Access + for location in tloz_world.levels[4].locations: + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_any(("Raft", "Recorder"), player)) - add_rule(world.get_location("Level 7 Item (Red Candle)", player), + # Digdogger boss. Rework this once ER happens + add_rule(tloz_world.get_location("Level 5 Boss"), lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Boss", player), + add_rule(tloz_world.get_location("Level 5 Triforce"), lambda state: state.has("Recorder", player)) - if options.ExpandedPool: - add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player), + + for location in gohma_locations: + if options.ExpandedPool or "Drop" not in location: + add_rule(tloz_world.get_location(location), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + # Recorder Access for Level 7 + for location in tloz_world.levels[7].locations: + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Recorder", player)) for location in food_locations: if options.ExpandedPool or "Drop" not in location: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Food", player)) for location in gleeok_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("swords", player) or state.has("Magical Rod", player)) # Candle access for Level 8 for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player)) - add_rule(world.get_location("Level 8 Item (Magical Key)", player), + add_rule(tloz_world.get_location("Level 8 Item (Magical Key)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) if options.ExpandedPool: - add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), + add_rule(tloz_world.get_location("Level 8 Bomb Drop (Darknuts North)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) for location in tloz_world.levels[9].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Triforce Fragment", player, 8) and state.has_group("swords", player)) # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop for level in range(1, 9): - add_rule(world.get_location(f"Level {level} Triforce", player), + add_rule(tloz_world.get_location(f"Level {level} Triforce"), lambda state, l=level: state.has(f"Boss {l} Defeated", player)) # Sword, raft, and ladder spots - add_rule(world.get_location("White Sword Pond", player), + add_rule(tloz_world.get_location("White Sword Pond"), lambda state: state.has("Heart Container", player, 2)) - add_rule(world.get_location("Magical Sword Grave", player), + add_rule(tloz_world.get_location("Magical Sword Grave"), lambda state: state.has("Heart Container", player, 9)) stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] for location in stepladder_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) if options.ExpandedPool: for location in stepladder_locations_expanded: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) # Don't allow Take Any Items until we can actually get in one if options.ExpandedPool: - add_rule(world.get_location("Take Any Item Left", player), + add_rule(tloz_world.get_location("Take Any Item Left"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Middle", player), + add_rule(tloz_world.get_location("Take Any Item Middle"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Right", player), + add_rule(tloz_world.get_location("Take Any Item Right"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - for location in tloz_world.levels[4].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Raft", player) or state.has("Recorder", player)) - for location in tloz_world.levels[7].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Recorder", player)) - for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Bow", player)) - add_rule(world.get_location("Potion Shop Item Left", player), + add_rule(tloz_world.get_location("Potion Shop Item Left"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Middle", player), + add_rule(tloz_world.get_location("Potion Shop Item Middle"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Right", player), + add_rule(tloz_world.get_location("Potion Shop Item Right"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Shield Shop Item Left", player), + add_rule(tloz_world.get_location("Shield Shop Item Left"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Middle", player), + add_rule(tloz_world.get_location("Shield Shop Item Middle"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Right", player), + add_rule(tloz_world.get_location("Shield Shop Item Right"), lambda state: state.has_group("candles", player) or - state.has("Bomb", player)) \ No newline at end of file + state.has("Bomb", player)) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index a1f9081418e4..c8c76bd85a8a 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -12,7 +12,8 @@ from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations from .Items import item_table, item_prices, item_game_ids from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ - standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations + standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations, \ + take_any_locations, sword_cave_locations from .Options import TlozOptions from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late from .Rules import set_rules @@ -87,6 +88,21 @@ class TLoZWorld(World): } } + location_name_groups = { + "Shops": set(shop_locations), + "Take Any": set(take_any_locations), + "Sword Caves": set(sword_cave_locations), + "Level 1": set(level_locations[0]), + "Level 2": set(level_locations[1]), + "Level 3": set(level_locations[2]), + "Level 4": set(level_locations[3]), + "Level 5": set(level_locations[4]), + "Level 6": set(level_locations[5]), + "Level 7": set(level_locations[6]), + "Level 8": set(level_locations[7]), + "Level 9": set(level_locations[8]) + } + for k, v in item_name_to_id.items(): item_name_to_id[k] = v + base_id @@ -94,8 +110,8 @@ class TLoZWorld(World): if v is not None: location_name_to_id[k] = v + base_id - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) self.generator_in_use = threading.Event() self.rom_name_available_event = threading.Event() self.levels = None @@ -307,7 +323,7 @@ def modify_multidata(self, multidata: dict): def get_filler_item_name(self) -> str: if self.filler_items is None: self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler] - return self.multiworld.random.choice(self.filler_items) + return self.random.choice(self.filler_items) def fill_slot_data(self) -> Dict[str, Any]: if self.options.ExpandedPool: diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 92834d96b07f..29dbf150125c 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,17 +1,28 @@ -from typing import Dict, List, Any, Tuple, TypedDict +from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union from logging import warning -from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld -from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState +from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names, + combat_items) from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon 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 .combat_logic import area_data, CombatState from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP +from settings import Group, Bool + + +class TunicSettings(Group): + class DisableLocalSpoiler(Bool): + """Disallows the TUNIC client from creating a local spoiler log.""" + + disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False class TunicWeb(WebWorld): @@ -40,10 +51,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): @@ -57,6 +70,7 @@ class TunicWorld(World): options: TunicOptions options_dataclass = TunicOptions + settings: ClassVar[TunicSettings] item_name_groups = item_name_groups location_name_groups = location_name_groups @@ -68,8 +82,22 @@ 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 + + # so we only loop the multiworld locations once + # if these are locations instead of their info, it gives a memory leak error + item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {} + player_item_link_locations: Dict[str, List[Location]] 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 @@ -90,7 +118,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"] @@ -98,36 +129,55 @@ def generate_early(self) -> None: self.options.shuffle_ladders.value = passthrough["shuffle_ladders"] self.options.fixed_shop.value = self.options.fixed_shop.option_false self.options.laurels_location.value = self.options.laurels_location.option_anywhere + self.options.combat_logic.value = passthrough["combat_logic"] @classmethod def stage_generate_early(cls, multiworld: MultiWorld) -> None: tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC") for tunic in tunic_worlds: + # setting up state combat logic stuff, see has_combat_reqs for its use + # and this is magic so pycharm doesn't like it, unfortunately + if tunic.options.combat_logic: + multiworld.state.tunic_need_to_reset_combat_from_collect[tunic.player] = False + multiworld.state.tunic_need_to_reset_combat_from_remove[tunic.player] = False + multiworld.state.tunic_area_combat_state[tunic.player] = {} + for area_name in area_data.keys(): + multiworld.state.tunic_area_combat_state[tunic.player][area_name] = CombatState.unchecked + # if it's one of the options, then it isn't a custom seed group if tunic.options.entrance_rando.value in EntranceRando.options.values(): continue 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=multiworld.plando_connections[tunic.player]) + 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.logic_rules.value < cls.seed_groups[group]["logic_rules"]: - cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value + 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.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 - if multiworld.plando_connections[tunic.player]: + if tunic.options.plando_connections: # loop through the connections in the player's yaml - for cxn in multiworld.plando_connections[tunic.player]: + for cxn in tunic.options.plando_connections: new_cxn = True for group_cxn in cls.seed_groups[group]["plando"]: # if neither entrance nor exit match anything in the group, add to group @@ -146,17 +196,18 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: if is_mismatched: raise Exception(f"TUNIC: Conflict between seed group {group}'s plando " f"connection {group_cxn.entrance} <-> {group_cxn.exit} and " - f"{tunic.multiworld.get_player_name(tunic.player)}'s plando " - f"connection {cxn.entrance} <-> {cxn.exit}") + f"{tunic.player_name}'s plando connection {cxn.entrance} <-> {cxn.exit}") if new_cxn: cls.seed_groups[group]["plando"].value.append(cxn) - def create_item(self, name: str) -> TunicItem: + def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem: item_data = item_table[name] - return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) + # if item_data.combat_ic is None, it'll take item_data.classification instead + itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None) + or item_data.classification) + return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player) def create_items(self) -> None: - tunic_items: List[TunicItem] = [] self.slot_data_items = [] @@ -178,19 +229,17 @@ def create_items(self) -> None: if self.options.laurels_location: laurels = self.create_item("Hero's Laurels") if self.options.laurels_location == "6_coins": - self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels) + self.get_location("Coins in the Well - 6 Coins").place_locked_item(laurels) elif self.options.laurels_location == "10_coins": - self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) + self.get_location("Coins in the Well - 10 Coins").place_locked_item(laurels) elif self.options.laurels_location == "10_fairies": - self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels) - self.slot_data_items.append(laurels) + self.get_location("Secret Gathering Place - 10 Fairy Reward").place_locked_item(laurels) items_to_create["Hero's Laurels"] = 0 if self.options.keys_behind_bosses: for rgb_hexagon, location in hexagon_locations.items(): hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon) - self.multiworld.get_location(location, self.player).place_locked_item(hex_item) - self.slot_data_items.append(hex_item) + self.get_location(location).place_locked_item(hex_item) items_to_create[rgb_hexagon] = 0 items_to_create[gold_hexagon] -= 3 @@ -200,7 +249,7 @@ def create_items(self) -> None: # Remove filler to make room for other items def remove_filler(amount: int) -> None: - for _ in range(0, amount): + for _ in range(amount): if not available_filler: fill = "Fool Trap" else: @@ -236,33 +285,41 @@ def remove_filler(amount: int) -> None: remove_filler(items_to_create[gold_hexagon]) for hero_relic in item_name_groups["Hero Relics"]: - relic_item = TunicItem(hero_relic, ItemClassification.useful, self.item_name_to_id[hero_relic], self.player) - tunic_items.append(relic_item) + tunic_items.append(self.create_item(hero_relic, ItemClassification.useful)) items_to_create[hero_relic] = 0 if not self.options.ability_shuffling: for page in item_name_groups["Abilities"]: if items_to_create[page] > 0: - page_item = TunicItem(page, ItemClassification.useful, self.item_name_to_id[page], self.player) - tunic_items.append(page_item) + tunic_items.append(self.create_item(page, ItemClassification.useful)) items_to_create[page] = 0 + # if ice grapple logic is on, probably really want icebolt + elif self.options.ice_grappling: + page = "Pages 52-53 (Icebolt)" + if items_to_create[page] > 0: + tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful)) + items_to_create[page] = 0 + + # logically relevant if you have ladder storage enabled + if self.options.ladder_storage and not self.options.ladder_storage_without_items: + tunic_items.append(self.create_item("Shield", ItemClassification.progression)) + items_to_create["Shield"] = 0 if self.options.maskless: - mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player) - tunic_items.append(mask_item) + tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful)) items_to_create["Scavenger Mask"] = 0 if self.options.lanternless: - lantern_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) - tunic_items.append(lantern_item) + tunic_items.append(self.create_item("Lantern", ItemClassification.useful)) items_to_create["Lantern"] = 0 for item, quantity in items_to_create.items(): - for i in range(0, quantity): - tunic_item: TunicItem = self.create_item(item) - if item in slot_data_item_names: - self.slot_data_items.append(tunic_item) - tunic_items.append(tunic_item) + for _ in range(quantity): + tunic_items.append(self.create_item(item)) + + for tunic_item in tunic_items: + if tunic_item.name in slot_data_item_names: + self.slot_data_items.append(tunic_item) self.multiworld.itempool += tunic_items @@ -279,44 +336,58 @@ def create_regions(self) -> None: self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"] self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"] - # ladder rando uses ER with vanilla connections, so that we're not managing more rules files - if self.options.entrance_rando or self.options.shuffle_ladders: + # Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance + if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic: portal_pairs = create_er_regions(self) if self.options.entrance_rando: # these get interpreted by the game to tell it which entrances to connect for portal1, portal2 in portal_pairs.items(): self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() else: - # for non-ER, non-ladders + # uses the original rules, easier to navigate and reference for region_name in tunic_regions: region = Region(region_name, self.player, self.multiworld) self.multiworld.regions.append(region) for region_name, exits in tunic_regions.items(): - region = self.multiworld.get_region(region_name, self.player) + region = self.get_region(region_name) region.add_exits(exits) for location_name, location_id in self.location_name_to_id.items(): - region = self.multiworld.get_region(location_table[location_name].region, self.player) + region = self.get_region(location_table[location_name].region) location = TunicLocation(self.player, location_name, location_id, region) region.locations.append(location) - victory_region = self.multiworld.get_region("Spirit Arena", self.player) + victory_region = self.get_region("Spirit Arena") victory_location = TunicLocation(self.player, "The Heir", None, victory_region) victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player)) self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) victory_region.locations.append(victory_location) def set_rules(self) -> None: - if self.options.entrance_rando or self.options.shuffle_ladders: - set_er_location_rules(self, self.ability_unlocks) + # same reason as in create_regions, could probably be put into create_regions + if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic: + set_er_location_rules(self) else: - set_region_rules(self, self.ability_unlocks) - set_location_rules(self, self.ability_unlocks) + set_region_rules(self) + set_location_rules(self) def get_filler_item_name(self) -> str: return self.random.choice(filler_items) + # cache whether you can get through combat logic areas + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change and self.options.combat_logic and item.name in combat_items: + state.tunic_need_to_reset_combat_from_collect[self.player] = True + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change and self.options.combat_logic and item.name in combat_items: + state.tunic_need_to_reset_combat_from_remove[self.player] = True + return change + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if self.options.entrance_rando: hint_data.update({self.player: {}}) @@ -335,10 +406,9 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: name, connection = paths[location.parent_region] 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 " - f"{self.multiworld.get_file_safe_player_name(self.player)}. " - "Creating entrance hint Inaccessible. " - "Please report this to the TUNIC rando devs.") + warning(f"{location.name} is not logically accessible for {self.player_name}. " + "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): @@ -355,6 +425,18 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if hint_text: hint_data[self.player][location.address] = hint_text + def get_real_location(self, location: Location) -> Tuple[str, int]: + # if it's not in a group, it's not in an item link + if location.player not in self.multiworld.groups or not location.item: + return location.name, location.player + try: + loc = self.player_item_link_locations[location.item.name].pop() + return loc.name, loc.player + except IndexError: + warning(f"TUNIC: Failed to parse item location for in-game hints for {self.player_name}. " + f"Using a potentially incorrect location name instead.") + return location.name, location.player + def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { "seed": self.random.randint(0, 2147483647), @@ -364,30 +446,58 @@ 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)), "shuffle_ladders": self.options.shuffle_ladders.value, + "combat_logic": self.options.combat_logic.value, "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], "Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"], "Hexagon Quest Goal": self.options.hexagon_goal.value, - "Entrance Rando": self.tunic_portal_pairs + "Entrance Rando": self.tunic_portal_pairs, + "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), } + # this would be in a stage if there was an appropriate stage for it + self.player_item_link_locations = {} + groups = self.multiworld.get_player_groups(self.player) + # checking if groups so that this doesn't run if the player isn't in a group + if groups: + if not self.item_link_locations: + tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC") + # figure out our groups and the items in them + for tunic in tunic_worlds: + for group in self.multiworld.get_player_groups(tunic.player): + self.item_link_locations.setdefault(group, {}) + for location in self.multiworld.get_locations(): + if location.item and location.item.player in self.item_link_locations.keys(): + (self.item_link_locations[location.item.player].setdefault(location.item.name, []) + .append((location.player, location.name))) + + # if item links are on, set up the player's personal item link locations, so we can pop them as needed + for group, item_links in self.item_link_locations.items(): + if group in groups: + for item_name, locs in item_links.items(): + self.player_item_link_locations[item_name] = \ + [self.multiworld.get_location(location_name, player) for player, location_name in locs] + for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): if tunic_item.name not in slot_data: slot_data[tunic_item.name] = [] if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: continue - slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) + slot_data[tunic_item.name].extend(self.get_real_location(tunic_item.location)) for start_item in self.options.start_inventory_from_pool: if start_item in slot_data_item_names: if start_item not in slot_data: slot_data[start_item] = [] - for i in range(0, self.options.start_inventory_from_pool[start_item]): + for _ in range(self.options.start_inventory_from_pool[start_item]): slot_data[start_item].extend(["Your Pocket", self.player]) for plando_item in self.multiworld.plando_items[self.player]: @@ -400,12 +510,14 @@ def fill_slot_data(self) -> Dict[str, Any]: if item in slot_data_item_names: slot_data[item] = [] for item_location in self.multiworld.find_item_locations(item, self.player): - slot_data[item].extend([item_location.name, item_location.player]) + slot_data[item].extend(self.get_real_location(item_location)) return slot_data # for the universal tracker, doesn't get called in standard gen + # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md @staticmethod def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough + # we are using re_gen_passthrough over modifying the world here due to complexities with ER return slot_data diff --git a/worlds/tunic/combat_logic.py b/worlds/tunic/combat_logic.py new file mode 100644 index 000000000000..9ff363942c9e --- /dev/null +++ b/worlds/tunic/combat_logic.py @@ -0,0 +1,422 @@ +from typing import Dict, List, NamedTuple, Tuple, Optional +from enum import IntEnum +from collections import defaultdict +from BaseClasses import CollectionState +from .rules import has_sword, has_melee +from worlds.AutoWorld import LogicMixin + + +# the vanilla stats you are expected to have to get through an area, based on where they are in vanilla +class AreaStats(NamedTuple): + att_level: int + def_level: int + potion_level: int # all 3 are before your first bonfire after getting the upgrade page, third costs 1k + hp_level: int + sp_level: int + mp_level: int + potion_count: int + equipment: List[str] = [] + is_boss: bool = False + + +# the vanilla upgrades/equipment you would have +area_data: Dict[str, AreaStats] = { + "Overworld": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Stick"]), + "East Forest": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Sword"]), + "Before Well": AreaStats(1, 1, 1, 1, 1, 1, 3, ["Sword", "Shield"]), + # learn how to upgrade + "Beneath the Well": AreaStats(2, 1, 3, 3, 1, 1, 3, ["Sword", "Shield"]), + "Dark Tomb": AreaStats(2, 2, 3, 3, 1, 1, 3, ["Sword", "Shield"]), + "West Garden": AreaStats(2, 3, 3, 3, 1, 1, 4, ["Sword", "Shield"]), + "Garden Knight": AreaStats(3, 3, 3, 3, 2, 1, 4, ["Sword", "Shield"], is_boss=True), + # get the wand here + "Beneath the Vault": AreaStats(3, 3, 3, 3, 2, 1, 4, ["Sword", "Shield", "Magic"]), + "Eastern Vault Fortress": AreaStats(3, 3, 3, 4, 3, 2, 4, ["Sword", "Shield", "Magic"]), + "Siege Engine": AreaStats(3, 3, 3, 4, 3, 2, 4, ["Sword", "Shield", "Magic"], is_boss=True), + "Frog's Domain": AreaStats(3, 4, 3, 5, 3, 3, 4, ["Sword", "Shield", "Magic"]), + # the second half of Atoll is the part you need the stats for, so putting it after frogs + "Ruined Atoll": AreaStats(4, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"]), + "The Librarian": AreaStats(4, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"], is_boss=True), + "Quarry": AreaStats(5, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"]), + "Rooted Ziggurat": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"]), + "Boss Scavenger": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"], is_boss=True), + "Swamp": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]), + "Cathedral": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]), + # marked as boss because the garden knights can't get hurt by stick + "Gauntlet": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"], is_boss=True), + "The Heir": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic", "Laurels"], is_boss=True), +} + + +# these are used for caching which areas can currently be reached in state +boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"] +non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss] + + +class CombatState(IntEnum): + unchecked = 0 + failed = 1 + succeeded = 2 + + +def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool: + # we're caching whether you've met the combat reqs before if the state didn't change first + # if the combat state is stale, mark each area's combat state as stale + if state.tunic_need_to_reset_combat_from_collect[player]: + state.tunic_need_to_reset_combat_from_collect[player] = False + for name in area_data.keys(): + if state.tunic_area_combat_state[player][name] == CombatState.failed: + state.tunic_area_combat_state[player][name] = CombatState.unchecked + + if state.tunic_need_to_reset_combat_from_remove[player]: + state.tunic_need_to_reset_combat_from_remove[player] = False + for name in area_data.keys(): + if state.tunic_area_combat_state[player][name] == CombatState.succeeded: + state.tunic_area_combat_state[player][name] = CombatState.unchecked + + if state.tunic_area_combat_state[player][area_name] > CombatState.unchecked: + return state.tunic_area_combat_state[player][area_name] == CombatState.succeeded + + met_combat_reqs = check_combat_reqs(area_name, state, player) + + # we want to skip the "none area" since we don't record its results + if area_name not in area_data.keys(): + return met_combat_reqs + + # loop through the lists and set the easier/harder area states accordingly + if area_name in boss_areas: + area_list = boss_areas + elif area_name in non_boss_areas: + area_list = non_boss_areas + else: + area_list = [area_name] + + if met_combat_reqs: + # set the state as true for each area until you get to the area we're looking at + for name in area_list: + state.tunic_area_combat_state[player][name] = CombatState.succeeded + if name == area_name: + break + else: + # set the state as false for the area we're looking at and each area after that + reached_name = False + for name in area_list: + if name == area_name: + reached_name = True + if reached_name: + state.tunic_area_combat_state[player][name] = CombatState.failed + + return met_combat_reqs + + +def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool: + data = alt_data or area_data[area_name] + extra_att_needed = 0 + extra_def_needed = 0 + extra_mp_needed = 0 + has_magic = state.has_any({"Magic Wand", "Gun"}, player) + stick_bool = False + sword_bool = False + for item in data.equipment: + if item == "Stick": + if not has_melee(state, player): + if has_magic: + # magic can make up for the lack of stick + extra_mp_needed += 2 + extra_att_needed -= 16 + else: + return False + else: + stick_bool = True + + elif item == "Sword": + if not has_sword(state, player): + # need sword for bosses + if data.is_boss: + return False + if has_magic: + # +4 mp pretty much makes up for the lack of sword, at least in Quarry + extra_mp_needed += 4 + # stick is a backup plan, and doesn't scale well, so let's require a little less + extra_att_needed -= 2 + elif has_melee(state, player): + # may revise this later based on feedback + extra_att_needed += 3 + extra_def_needed += 2 + else: + return False + else: + sword_bool = True + + elif item == "Shield": + if not state.has("Shield", player): + extra_def_needed += 2 + elif item == "Laurels": + if not state.has("Hero's Laurels", player): + # these are entirely based on vibes + extra_att_needed += 2 + extra_def_needed += 3 + elif item == "Magic": + if not has_magic: + extra_att_needed += 2 + extra_def_needed += 2 + extra_mp_needed -= 16 + modified_stats = AreaStats(data.att_level + extra_att_needed, data.def_level + extra_def_needed, data.potion_level, + data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count) + if not has_required_stats(modified_stats, state, player): + # we may need to check if you would have the required stats if you were missing a weapon + # it's kinda janky, but these only get hit in less than once per 100 generations, so whatever + if sword_bool and "Sword" in data.equipment and "Magic" in data.equipment: + # we need to check if you would have the required stats if you didn't have melee + equip_list = [item for item in data.equipment if item != "Sword"] + more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level, + data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count, + equip_list) + if check_combat_reqs("none", state, player, more_modified_stats): + return True + + # and we need to check if you would have the required stats if you didn't have magic + equip_list = [item for item in data.equipment if item != "Magic"] + more_modified_stats = AreaStats(data.att_level + 2, data.def_level + 2, data.potion_level, + data.hp_level, data.sp_level, data.mp_level - 16, data.potion_count, + equip_list) + if check_combat_reqs("none", state, player, more_modified_stats): + return True + return False + + elif stick_bool and "Stick" in data.equipment and "Magic" in data.equipment: + # we need to check if you would have the required stats if you didn't have the stick + equip_list = [item for item in data.equipment if item != "Stick"] + more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level, + data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count, + equip_list) + if check_combat_reqs("none", state, player, more_modified_stats): + return True + return False + else: + return False + return True + + +# check if you have the required stats, and the money to afford them +# it may be innaccurate due to poor spending, and it may even require you to "spend poorly" +# but that's fine -- it's already pretty generous to begin with +def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> bool: + money_required = 0 + player_att = 0 + + # check if we actually need the stat before checking state + if data.att_level > 1: + player_att, att_offerings = get_att_level(state, player) + if player_att < data.att_level: + return False + else: + extra_att = player_att - data.att_level + paid_att = max(0, att_offerings - extra_att) + # attack upgrades cost 100 for the first, +50 for each additional + money_per_att = 100 + for _ in range(paid_att): + money_required += money_per_att + money_per_att += 50 + + # adding defense and sp together since they accomplish similar things: making you take less damage + if data.def_level + data.sp_level > 2: + player_def, def_offerings = get_def_level(state, player) + player_sp, sp_offerings = get_sp_level(state, player) + if player_def + player_sp < data.def_level + data.sp_level: + return False + else: + free_def = player_def - def_offerings + free_sp = player_sp - sp_offerings + paid_stats = data.def_level + data.sp_level - free_def - free_sp + sp_to_buy = 0 + + if paid_stats <= 0: + # if you don't have to pay for any stats, you don't need money for these upgrades + def_to_buy = 0 + elif paid_stats <= def_offerings: + # get the amount needed to buy these def offerings + def_to_buy = paid_stats + else: + def_to_buy = def_offerings + sp_to_buy = max(0, paid_stats - def_offerings) + + # if you have to buy more than 3 def, it's cheaper to buy 1 extra sp + if def_to_buy > 3 and sp_offerings > 0: + def_to_buy -= 1 + sp_to_buy += 1 + # def costs 100 for the first, +50 for each additional + money_per_def = 100 + for _ in range(def_to_buy): + money_required += money_per_def + money_per_def += 50 + # sp costs 200 for the first, +200 for each additional + money_per_sp = 200 + for _ in range(sp_to_buy): + money_required += money_per_sp + money_per_sp += 200 + + # if you have 2 more attack than needed, we can forego needing mp + if data.mp_level > 1 and player_att < data.att_level + 2: + player_mp, mp_offerings = get_mp_level(state, player) + if player_mp < data.mp_level: + return False + else: + extra_mp = player_mp - data.mp_level + paid_mp = max(0, mp_offerings - extra_mp) + # mp costs 300 for the first, +50 for each additional + money_per_mp = 300 + for _ in range(paid_mp): + money_required += money_per_mp + money_per_mp += 50 + + req_effective_hp = calc_effective_hp(data.hp_level, data.potion_level, data.potion_count) + player_potion, potion_offerings = get_potion_level(state, player) + player_hp, hp_offerings = get_hp_level(state, player) + player_potion_count = get_potion_count(state, player) + player_effective_hp = calc_effective_hp(player_hp, player_potion, player_potion_count) + if player_effective_hp < req_effective_hp: + return False + else: + # need a way to determine which of potion offerings or hp offerings you can reduce + # your level if you didn't pay for offerings + free_potion = player_potion - potion_offerings + free_hp = player_hp - hp_offerings + paid_hp_count = 0 + paid_potion_count = 0 + if calc_effective_hp(free_hp, free_potion, player_potion_count) >= req_effective_hp: + # you don't need to buy upgrades + pass + # if you have no potions, or no potion upgrades, you only need to check your hp upgrades + elif player_potion_count == 0 or potion_offerings == 0: + # check if you have enough hp at each paid hp offering + for i in range(hp_offerings): + paid_hp_count = i + 1 + if calc_effective_hp(paid_hp_count, 0, player_potion_count) > req_effective_hp: + break + else: + for i in range(potion_offerings): + paid_potion_count = i + 1 + if calc_effective_hp(free_hp, free_potion + paid_potion_count, player_potion_count) > req_effective_hp: + break + for j in range(hp_offerings): + paid_hp_count = j + 1 + if (calc_effective_hp(free_hp + paid_hp_count, free_potion + paid_potion_count, player_potion_count) + > req_effective_hp): + break + # hp costs 200 for the first, +50 for each additional + money_per_hp = 200 + for _ in range(paid_hp_count): + money_required += money_per_hp + money_per_hp += 50 + + # potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional + # currently we assume you will not buy past the second potion upgrade, but we might change our minds later + money_per_potion = 100 + for _ in range(paid_potion_count): + money_required += money_per_potion + if money_per_potion == 100: + money_per_potion = 300 + elif money_per_potion == 300: + money_per_potion = 1000 + else: + money_per_potion += 200 + + if money_required > get_money_count(state, player): + return False + + return True + + +# returns a tuple of your max attack level, the number of attack offerings +def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]: + att_offerings = state.count("ATT Offering", player) + att_upgrades = state.count("Hero Relic - ATT", player) + sword_level = state.count("Sword Upgrade", player) + if sword_level >= 3: + att_upgrades += min(2, sword_level - 2) + # attack falls off, can just cap it at 8 for simplicity + return min(8, 1 + att_offerings + att_upgrades), att_offerings + + +# returns a tuple of your max defense level, the number of defense offerings +def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]: + def_offerings = state.count("DEF Offering", player) + # defense falls off, can just cap it at 8 for simplicity + return (min(8, 1 + def_offerings + + state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player)), + def_offerings) + + +# returns a tuple of your max potion level, the number of potion offerings +def get_potion_level(state: CollectionState, player: int) -> Tuple[int, int]: + potion_offerings = min(2, state.count("Potion Offering", player)) + # your third potion upgrade (from offerings) costs 1,000 money, reasonable to assume you won't do that + return (1 + potion_offerings + + state.count_from_list({"Hero Relic - POTION", "Just Some Pals", "Spring Falls", "Back To Work"}, player), + potion_offerings) + + +# returns a tuple of your max hp level, the number of hp offerings +def get_hp_level(state: CollectionState, player: int) -> Tuple[int, int]: + hp_offerings = state.count("HP Offering", player) + return 1 + hp_offerings + state.count("Hero Relic - HP", player), hp_offerings + + +# returns a tuple of your max sp level, the number of sp offerings +def get_sp_level(state: CollectionState, player: int) -> Tuple[int, int]: + sp_offerings = state.count("SP Offering", player) + return (1 + sp_offerings + + state.count_from_list({"Hero Relic - SP", "Mr Mayor", "Power Up", + "Regal Weasel", "Forever Friend"}, player), + sp_offerings) + + +def get_mp_level(state: CollectionState, player: int) -> Tuple[int, int]: + mp_offerings = state.count("MP Offering", player) + return (1 + mp_offerings + + state.count_from_list({"Hero Relic - MP", "Sacred Geometry", "Vintage", "Dusty"}, player), + mp_offerings) + + +def get_potion_count(state: CollectionState, player: int) -> int: + return state.count("Potion Flask", player) + state.count("Flask Shard", player) // 3 + + +def calc_effective_hp(hp_level: int, potion_level: int, potion_count: int) -> int: + player_hp = 60 + hp_level * 20 + # since you don't tend to use potions efficiently all the time, scale healing by .75 + total_healing = int(.75 * potion_count * min(player_hp, 20 + 10 * potion_level)) + return player_hp + total_healing + + +# returns the total amount of progression money the player has +def get_money_count(state: CollectionState, player: int) -> int: + money: int = 0 + # this could be done with something to parse the money count at the end of the string, but I don't wanna + money += state.count("Money x255", player) * 255 # 1 in pool + money += state.count("Money x200", player) * 200 # 1 in pool + money += state.count("Money x128", player) * 128 # 3 in pool + # total from regular money: 839 + # first effigy is 8, doubles until it reaches 512 at number 7, after effigy 28 they stop dropping money + # with the vanilla count of 12, you get 3,576 money from effigies + effigy_count = min(28, state.count("Effigy", player)) # 12 in pool + money_per_break = 8 + for _ in range(effigy_count): + money += money_per_break + money_per_break = min(512, money_per_break * 2) + return money + + +class TunicState(LogicMixin): + tunic_need_to_reset_combat_from_collect: Dict[int, bool] + tunic_need_to_reset_combat_from_remove: Dict[int, bool] + tunic_area_combat_state: Dict[int, Dict[str, int]] + + def init_mixin(self, _): + # the per-player need to reset the combat state when collecting a combat item + self.tunic_need_to_reset_combat_from_collect = defaultdict(lambda: False) + # the per-player need to reset the combat state when removing a combat item + self.tunic_need_to_reset_combat_from_remove = defaultdict(lambda: False) + # the per-player, per-area state of combat checking -- unchecked, failed, or succeeded + self.tunic_area_combat_state = defaultdict(lambda: defaultdict(lambda: CombatState.unchecked)) diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 27df4ce38be4..ab751d8e669d 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -56,6 +56,7 @@ In general: - Bushes are not considered in logic. It is assumed that the player will find a way past them, whether it is with a sword, a bomb, fire, luring an enemy, etc. There is also an option in the in-game randomizer settings menu to clear some of the early bushes. - The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. - The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. +- For the Ice Grappling, Ladder Storage, and Laurels Zips options, there is [this document](https://docs.google.com/document/d/1SFZBfsqZWH1_EAV9zyZobvrBcvCd3_54JP3iVnJ8rUg/edit?usp=sharing) that shows the individual applications of these tricks in logic. For the Entrance Randomizer: - Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore. @@ -83,8 +84,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 f49e7dff3e58..9794f4a87b67 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 @@ -75,7 +80,7 @@ def destination_scene(self) -> str: # the vanilla connection destination="Town Basement", tag="_beach"), Portal(name="Changing Room Entrance", region="Overworld", destination="Changing Room", tag="_"), - Portal(name="Cube Cave Entrance", region="Overworld", + Portal(name="Cube Cave Entrance", region="Cube Cave Entrance Region", destination="CubeRoom", tag="_"), Portal(name="Stairs from Overworld to Mountain", region="Upper Overworld", destination="Mountain", tag="_"), @@ -169,100 +174,16 @@ def destination_scene(self) -> str: # the vanilla connection destination="Overworld Redux", tag="_rafters"), Portal(name="Temple Door Exit", region="Sealed Temple", destination="Overworld Redux", tag="_main"), - - Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit", - destination="Overworld Redux", tag="_entrance"), - Portal(name="Well to Well Boss", region="Beneath the Well Back", - destination="Sewer_Boss", tag="_"), - Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", - destination="Overworld Redux", tag="_west_aqueduct"), - - Portal(name="Well Boss to Well", region="Well Boss", - destination="Sewer", tag="_"), - Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", - destination="Crypt Redux", tag="_"), - - Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", + + Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", + destination="Fortress Courtyard", tag="_"), + Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", + destination="East Forest Redux", tag="_"), + Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", destination="Overworld Redux", tag="_"), - Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", - destination="Furnace", tag="_"), - Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", - destination="Sewer_Boss", tag="_"), - - Portal(name="West Garden Exit near Hero's Grave", region="West Garden", - destination="Overworld Redux", tag="_lower"), - Portal(name="West Garden to Magic Dagger House", region="West Garden", - destination="archipelagos_house", tag="_"), - Portal(name="West Garden Exit after Boss", region="West Garden after Boss", - destination="Overworld Redux", tag="_upper"), - Portal(name="West Garden Shop", region="West Garden", - destination="Shop", tag="_"), - Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region", - destination="Overworld Redux", tag="_lowest"), - Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth"), - Portal(name="West Garden to Far Shore", region="West Garden Portal", - destination="Transit", tag="_teleporter_archipelagos_teleporter"), - - Portal(name="Magic Dagger House Exit", region="Magic Dagger House", - destination="Archipelagos Redux", tag="_"), - - Portal(name="Atoll Upper Exit", region="Ruined Atoll", - destination="Overworld Redux", tag="_upper"), - Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", - destination="Overworld Redux", tag="_lower"), - Portal(name="Atoll Shop", region="Ruined Atoll", - destination="Shop", tag="_"), - Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", - destination="Transit", tag="_teleporter_atoll"), - Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue", - destination="Library Exterior", tag="_"), - Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye", - destination="Frog Stairs", tag="_eye"), - Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", - destination="Frog Stairs", tag="_mouth"), - - Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", - destination="Atoll Redux", tag="_eye"), - Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", - destination="Atoll Redux", tag="_mouth"), - Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain", - destination="frog cave main", tag="_Entrance"), - Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", - destination="frog cave main", tag="_Exit"), - - Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", - destination="Frog Stairs", tag="_Entrance"), - Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", - destination="Frog Stairs", tag="_Exit"), - - Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", - destination="Atoll Redux", tag="_"), - Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", - destination="Library Hall", tag="_"), - - Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", - destination="Library Exterior", tag="_"), - Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth"), - Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", - destination="Library Rotunda", tag="_"), - - Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", - destination="Library Hall", tag="_"), - Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", - destination="Library Lab", tag="_"), - - Portal(name="Library Lab to Rotunda", region="Library Lab Lower", - destination="Library Rotunda", tag="_"), - Portal(name="Library to Far Shore", region="Library Portal", - destination="Transit", tag="_teleporter_library teleporter"), - Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", - destination="Library Arena", tag="_"), - - Portal(name="Librarian Arena Exit", region="Library Arena", - destination="Library Lab", tag="_"), - + Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", + destination="Forest Boss Room", tag="_"), + Portal(name="Forest to Belltower", region="East Forest", destination="Forest Belltower", tag="_"), Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", @@ -281,7 +202,14 @@ def destination_scene(self) -> str: # the vanilla connection destination="Sword Access", tag="_lower"), Portal(name="Forest Grave Path Upper Entrance", region="East Forest", destination="Sword Access", tag="_upper"), - + + Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", + destination="East Forest Redux", tag="_upper"), + Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", + destination="East Forest Redux", tag="_lower"), + Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", destination="East Forest Redux", tag="_upper"), Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", @@ -290,33 +218,54 @@ def destination_scene(self) -> str: # the vanilla connection destination="East Forest Redux", tag="_gate"), Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", destination="Forest Boss Room", tag="_"), - - Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", - destination="East Forest Redux", tag="_upper"), - Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", - destination="East Forest Redux", tag="_lower"), - Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", - destination="RelicVoid", tag="_teleporter_relic plinth"), - + Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower", destination="East Forest Redux", tag="_lower"), Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper", destination="East Forest Redux", tag="_upper"), - + Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", destination="East Forest Redux Laddercave", tag="_"), Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", destination="Forest Belltower", tag="_"), - - Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", - destination="Fortress Courtyard", tag="_"), - Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", - destination="East Forest Redux", tag="_"), - Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", + + Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit", + destination="Overworld Redux", tag="_entrance"), + Portal(name="Well to Well Boss", region="Beneath the Well Back", + destination="Sewer_Boss", tag="_"), + Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", + destination="Overworld Redux", tag="_west_aqueduct"), + + Portal(name="Well Boss to Well", region="Well Boss", + destination="Sewer", tag="_"), + Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", + destination="Crypt Redux", tag="_"), + + Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", destination="Overworld Redux", tag="_"), - Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", - destination="Forest Boss Room", tag="_"), + Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", + destination="Furnace", tag="_"), + Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", + destination="Sewer_Boss", tag="_"), + Portal(name="West Garden Exit near Hero's Grave", region="West Garden before Terry", + destination="Overworld Redux", tag="_lower"), + Portal(name="West Garden to Magic Dagger House", region="West Garden at Dagger House", + destination="archipelagos_house", tag="_"), + Portal(name="West Garden Exit after Boss", region="West Garden after Boss", + destination="Overworld Redux", tag="_upper"), + Portal(name="West Garden Shop", region="West Garden before Terry", + destination="Shop", tag="_"), + Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region", + destination="Overworld Redux", tag="_lowest"), + Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="West Garden to Far Shore", region="West Garden Portal", + destination="Transit", tag="_teleporter_archipelagos_teleporter"), + + Portal(name="Magic Dagger House Exit", region="Magic Dagger House", + destination="Archipelagos Redux", tag="_"), + Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", destination="Fortress Reliquary", tag="_Lower"), Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", @@ -333,12 +282,12 @@ def destination_scene(self) -> str: # the vanilla connection destination="Overworld Redux", tag="_"), Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", destination="Shop", tag="_"), - + Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back", destination="Fortress Main", tag="_"), Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit", destination="Fortress Courtyard", tag="_"), - + Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", destination="Fortress Courtyard", tag="_Big Door"), Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", @@ -351,15 +300,15 @@ def destination_scene(self) -> str: # the vanilla connection destination="Fortress East", tag="_upper"), Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", destination="Fortress East", tag="_lower"), - + Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", destination="Fortress Main", tag="_lower"), Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", destination="Fortress Courtyard", tag="_"), Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", destination="Fortress Main", tag="_upper"), - - Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", + + Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path Entry", destination="Fortress Courtyard", tag="_Lower"), Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region", destination="RelicVoid", tag="_teleporter_relic plinth"), @@ -370,12 +319,68 @@ def destination_scene(self) -> str: # the vanilla connection Portal(name="Dusty Exit", region="Fortress Leaf Piles", destination="Fortress Reliquary", tag="_"), - + Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", destination="Fortress Main", tag="_"), Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", destination="Transit", tag="_teleporter_spidertank"), - + + Portal(name="Atoll Upper Exit", region="Ruined Atoll", + destination="Overworld Redux", tag="_upper"), + Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", + destination="Overworld Redux", tag="_lower"), + Portal(name="Atoll Shop", region="Ruined Atoll", + destination="Shop", tag="_"), + Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", + destination="Transit", tag="_teleporter_atoll"), + Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue", + destination="Library Exterior", tag="_"), + Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye", + destination="Frog Stairs", tag="_eye"), + Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", + destination="Frog Stairs", tag="_mouth"), + + Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", + destination="Atoll Redux", tag="_eye"), + Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", + destination="Atoll Redux", tag="_mouth"), + Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain", + destination="frog cave main", tag="_Entrance"), + Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", + destination="frog cave main", tag="_Exit"), + + Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", + destination="Frog Stairs", tag="_Entrance"), + Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", + destination="Frog Stairs", tag="_Exit"), + + Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", + destination="Atoll Redux", tag="_"), + Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", + destination="Library Hall", tag="_"), + + Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", + destination="Library Exterior", tag="_"), + Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", + destination="Library Rotunda", tag="_"), + + Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", + destination="Library Hall", tag="_"), + Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", + destination="Library Lab", tag="_"), + + Portal(name="Library Lab to Rotunda", region="Library Lab Lower", + destination="Library Rotunda", tag="_"), + Portal(name="Library to Far Shore", region="Library Portal", + destination="Transit", tag="_teleporter_library teleporter"), + Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", + destination="Library Arena", tag="_"), + + Portal(name="Librarian Arena Exit", region="Library Arena", + destination="Library Lab", tag="_"), + Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", destination="Mountaintop", tag="_"), Portal(name="Mountain to Quarry", region="Lower Mountain", @@ -428,7 +433,7 @@ def destination_scene(self) -> str: # the vanilla connection Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom", destination="ziggurat2020_3", tag="_"), - Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front", + Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Entry", destination="ziggurat2020_2", tag="_"), Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", destination="ziggurat2020_FTRoom", tag="_"), @@ -456,9 +461,9 @@ def destination_scene(self) -> str: # the vanilla connection Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region", destination="RelicVoid", tag="_teleporter_relic plinth"), - Portal(name="Cathedral Main Exit", region="Cathedral", + Portal(name="Cathedral Main Exit", region="Cathedral Entry", 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,12 @@ 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 + + +# 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): @@ -551,6 +562,8 @@ class DeadEnd(IntEnum): "Overworld to West Garden Upper": RegionInfo("Overworld Redux"), # usually leads to garden knight "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux"), # isolated stairway with one chest "Overworld Well Ladder": RegionInfo("Overworld Redux"), # just the ladder entrance itself as a region + "Overworld Well Entry Area": RegionInfo("Overworld Redux"), # the page, the bridge, etc. + "Overworld Tunnel to Beach": RegionInfo("Overworld Redux"), # the tunnel with the chest "Overworld Beach": RegionInfo("Overworld Redux"), # from the two turrets to invisble maze, and lower atoll entry "Overworld Tunnel Turret": RegionInfo("Overworld Redux"), # the tunnel turret by the southwest beach ladder "Overworld to Atoll Upper": RegionInfo("Overworld Redux"), # the little ledge before the ladder @@ -558,10 +571,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 + "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 @@ -590,7 +604,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"), @@ -600,7 +614,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"), @@ -611,39 +625,47 @@ class DeadEnd(IntEnum): "Beneath the Well Front": RegionInfo("Sewer"), # the front, to separate it from the weapon requirement in the mid "Beneath the Well Main": RegionInfo("Sewer"), # the main section of it, requires a weapon "Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests - "West Garden": RegionInfo("Archipelagos Redux"), + "West Garden before Terry": RegionInfo("Archipelagos Redux"), # the lower entry point, near hero grave + "West Garden after Terry": RegionInfo("Archipelagos Redux"), # after Terry, up until next chompignons + "West Garden at Dagger House": RegionInfo("Archipelagos Redux"), # just outside magic dagger house + "West Garden South Checkpoint": 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 before Boss": RegionInfo("Archipelagos Redux"), # main west garden "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 before Terry"), "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"), "Frog Stairs to Frog's Domain": RegionInfo("Frog Stairs"), - "Frog's Domain Entry": RegionInfo("frog cave main"), - "Frog's Domain": RegionInfo("frog cave main"), + "Frog's Domain Entry": RegionInfo("frog cave main"), # just the ladder + "Frog's Domain Front": RegionInfo("frog cave main"), # before combat + "Frog's Domain Main": 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 Lab on Portal Pad": RegionInfo("Library Lab"), + "Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"), "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"), @@ -659,28 +681,31 @@ class DeadEnd(IntEnum): "Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"), "Fortress East Shortcut Upper": RegionInfo("Fortress East"), "Fortress East Shortcut Lower": RegionInfo("Fortress East"), - "Fortress Grave Path": RegionInfo("Fortress Reliquary"), + "Fortress Grave Path Entry": RegionInfo("Fortress Reliquary"), + "Fortress Grave Path Combat": RegionInfo("Fortress Reliquary"), # the combat is basically just a barrier here + "Fortress Grave Path by Grave": 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 by Grave"), "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"), + "Even Lower Quarry Isolated Chest": RegionInfo("Quarry Redux"), # a region for that one chest "Lower Quarry Zig Door": RegionInfo("Quarry Redux"), "Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"), "Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"), @@ -688,21 +713,26 @@ class DeadEnd(IntEnum): "Rooted Ziggurat Upper Back": RegionInfo("ziggurat2020_1"), # after the administrator "Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"), "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"), - "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side + "Rooted Ziggurat Lower Entry": RegionInfo("ziggurat2020_3"), # the vanilla entry point side + "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the front for combat logic + "Rooted Ziggurat Lower Mid Checkpoint": RegionInfo("ziggurat2020_3"), # the mid-checkpoint before double admin "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"), - "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), + "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry"), # 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", outlet_region="Rooted Ziggurat Portal Room"), "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 Entry": RegionInfo("Cathedral Redux"), # the checkpoint and easily-accessible chests + "Cathedral Main": RegionInfo("Cathedral Redux"), # the majority of Cathedral + "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"), @@ -710,10 +740,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), @@ -723,25 +753,37 @@ class DeadEnd(IntEnum): "Purgatory": RegionInfo("Purgatory"), "Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats), "Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats), - "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats) + "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats), } +# 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": [], + "Overworld Tunnel to Beach": + [], "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"]], - "Overworld Well Ladder": + [["Hyperdash"], ["LS1"]], + "Overworld Well Entry Area": [], "Overworld Ruined Passage Door": [], @@ -758,11 +800,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": @@ -772,23 +814,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"], ["LS1"]], }, "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": @@ -797,8 +844,8 @@ class DeadEnd(IntEnum): "Overworld Belltower": { "Overworld Belltower at Bell": [], - "Overworld": - [], + # "Overworld": + # [], "Overworld to West Garden Upper": [], }, @@ -806,19 +853,25 @@ 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 Tunnel to Beach": { + # "Overworld": + # [], + "Overworld Beach": [], }, "Overworld Beach": { - "Overworld": - [], + # "Overworld": + # [], "Overworld West Garden Laurels Entry": - [["Hyperdash"]], + [["Hyperdash"], ["LS1"]], "Overworld to Atoll Upper": [], "Overworld Tunnel Turret": @@ -829,38 +882,43 @@ class DeadEnd(IntEnum): [["Hyperdash"]], }, "Overworld to Atoll Upper": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, "Overworld Tunnel Turret": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, + "Overworld Well Entry Area": { + # "Overworld": + # [], + "Overworld Well Ladder": + [], + }, "Overworld Well Ladder": { - "Overworld": + "Overworld Well Entry Area": [], }, "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": @@ -875,73 +933,78 @@ 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 Ruined Passage Door": { + # "Overworld": + # [["Hyperdash", "Zip"]], + # }, + # "Overworld Town Portal": { + # "Overworld": + # [], + # }, + # "Overworld Spawn Portal": { + # "Overworld": + # [], + # }, + "Cube Cave Entrance Region": { "Overworld": [], }, + "Old House Front": { "Old House Back": [], }, "Old House Back": { "Old House Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, + "Furnace Fuse": { "Furnace Ladder Area": [["Hyperdash"]], }, "Furnace Ladder Area": { "Furnace Fuse": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Furnace Walking Path": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, "Furnace Walking Path": { "Furnace Ladder Area": [["Hyperdash"]], }, + "Sealed Temple": { "Sealed Temple Rafters": [], @@ -950,10 +1013,12 @@ class DeadEnd(IntEnum): "Sealed Temple": [["Hyperdash"]], }, + "Hourglass Cave": { "Hourglass Cave Tower": [], }, + "Forest Belltower Upper": { "Forest Belltower Main": [], @@ -962,9 +1027,10 @@ class DeadEnd(IntEnum): "Forest Belltower Lower": [], }, + "East Forest": { "East Forest Dance Fox Spot": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"], ["LS1"]], "East Forest Portal": [], "Lower Forest": @@ -972,7 +1038,7 @@ class DeadEnd(IntEnum): }, "East Forest Dance Fox Spot": { "East Forest": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"]], }, "East Forest Portal": { "East Forest": @@ -982,14 +1048,16 @@ class DeadEnd(IntEnum): "East Forest": [], }, + "Guard House 1 East": { "Guard House 1 West": [], }, "Guard House 1 West": { "Guard House 1 East": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, + "Guard House 2 Upper": { "Guard House 2 Lower": [], @@ -998,26 +1066,28 @@ class DeadEnd(IntEnum): "Guard House 2 Upper": [], }, + "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": [], }, + "Beneath the Well Ladder Exit": { "Beneath the Well Front": [], @@ -1038,14 +1108,16 @@ class DeadEnd(IntEnum): "Beneath the Well Main": [], }, + "Well Boss": { "Dark Tomb Checkpoint": [], }, "Dark Tomb Checkpoint": { "Well Boss": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, + "Dark Tomb Entry Point": { "Dark Tomb Upper": [], @@ -1066,41 +1138,75 @@ class DeadEnd(IntEnum): "Dark Tomb Main": [], }, - "West Garden": { + + "West Garden before Terry": { + "West Garden after Terry": + [], + "West Garden Hero's Grave Region": + [], + }, + "West Garden Hero's Grave Region": { + "West Garden before Terry": + [], + }, + "West Garden after Terry": { + "West Garden before Terry": + [], + "West Garden South Checkpoint": + [], "West Garden Laurels Exit Region": - [["Hyperdash"], ["UR"]], + [["LS1"]], + }, + "West Garden South Checkpoint": { + "West Garden before Boss": + [], + "West Garden at Dagger House": + [], + "West Garden after Terry": + [], + }, + "West Garden before Boss": { "West Garden after Boss": - [], - "West Garden Hero's Grave Region": + [], + "West Garden South Checkpoint": + [], + }, + "West Garden after Boss": { + "West Garden before Boss": + [["Hyperdash"]], + }, + "West Garden at Dagger House": { + "West Garden Laurels Exit Region": + [["Hyperdash"]], + "West Garden South Checkpoint": [], "West Garden Portal Item": - [["NMG"]], + [["IG2"]], }, "West Garden Laurels Exit Region": { - "West Garden": + "West Garden at Dagger House": [["Hyperdash"]], }, - "West Garden after Boss": { - "West Garden": + "West Garden Portal Item": { + "West Garden at Dagger House": + [["IG1"]], + "West Garden by Portal": [["Hyperdash"]], }, - "West Garden Portal Item": { - "West Garden": - [["NMG"]], + "West Garden by Portal": { "West Garden Portal": - [["Hyperdash", "West Garden"]], - }, - "West Garden Portal": { + [["West Garden South Checkpoint"]], "West Garden Portal Item": [["Hyperdash"]], }, - "West Garden Hero's Grave Region": { - "West Garden": + "West Garden Portal": { + "West Garden by Portal": [], }, + "Ruined Atoll": { "Ruined Atoll Lower Entry Area": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Ruined Atoll Ladder Tops": [], "Ruined Atoll Frog Mouth": @@ -1136,6 +1242,7 @@ class DeadEnd(IntEnum): "Ruined Atoll": [], }, + "Frog Stairs Eye Exit": { "Frog Stairs Upper": [], @@ -1156,24 +1263,40 @@ class DeadEnd(IntEnum): "Frog Stairs Lower": [], }, + "Frog's Domain Entry": { - "Frog's Domain": + "Frog's Domain Front": [], }, - "Frog's Domain": { + "Frog's Domain Front": { "Frog's Domain Entry": [], + "Frog's Domain Main": + [], + }, + "Frog's Domain Main": { + "Frog's Domain Front": + [], "Frog's Domain Back": [], }, + + # cannot get from frogs back to front "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": { "Library Hall": [], @@ -1183,6 +1306,8 @@ class DeadEnd(IntEnum): [], "Library Hero's Grave Region": [], + "Library Hall to Rotunda": + [], }, "Library Hero's Grave Region": { "Library Hall": @@ -1192,6 +1317,7 @@ class DeadEnd(IntEnum): "Library Hall": [], }, + "Library Rotunda to Hall": { "Library Rotunda": [], @@ -1214,44 +1340,49 @@ 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": [], }, + "Fortress Exterior from East Forest": { "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": - [["Hyperdash"]], + [["Hyperdash"]], "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": [], }, @@ -1261,7 +1392,7 @@ class DeadEnd(IntEnum): }, "Fortress Courtyard": { "Fortress Courtyard Upper": - [["NMG"]], + [["IG1"]], "Fortress Exterior from Overworld": [["Hyperdash"]], }, @@ -1269,6 +1400,7 @@ class DeadEnd(IntEnum): "Fortress Courtyard": [], }, + "Beneath the Vault Ladder Exit": { "Beneath the Vault Main": [], @@ -1285,40 +1417,62 @@ class DeadEnd(IntEnum): "Beneath the Vault Ladder Exit": [], }, + "Fortress East Shortcut Lower": { "Fortress East Shortcut Upper": - [["NMG"]], + [["IG1"]], }, "Fortress East Shortcut Upper": { "Fortress East Shortcut Lower": [], }, + "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 Entry": { + "Fortress Grave Path Combat": + [], + # redundant here, keeping a comment to show it's intentional + # "Fortress Grave Path Dusty Entrance Region": + # [["Hyperdash"]], + }, + "Fortress Grave Path Combat": { + "Fortress Grave Path Entry": + [], + "Fortress Grave Path by Grave": + [], }, - "Fortress Grave Path": { + "Fortress Grave Path by Grave": { + "Fortress Grave Path Entry": + [], + # unnecessary, you can just skip it + # "Fortress Grave Path Combat": + # [], "Fortress Hero's Grave Region": - [], + [], "Fortress Grave Path Dusty Entrance Region": [["Hyperdash"]], }, "Fortress Grave Path Upper": { - "Fortress Grave Path": - [["NMG"]], + "Fortress Grave Path Entry": + [["IG1"]], }, "Fortress Grave Path Dusty Entrance Region": { - "Fortress Grave Path": + "Fortress Grave Path by Grave": [["Hyperdash"]], }, "Fortress Hero's Grave Region": { - "Fortress Grave Path": + "Fortress Grave Path by Grave": [], }, + "Fortress Arena": { "Fortress Arena Portal": [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], @@ -1327,6 +1481,7 @@ class DeadEnd(IntEnum): "Fortress Arena": [], }, + "Lower Mountain": { "Lower Mountain Stairs": [], @@ -1335,9 +1490,10 @@ class DeadEnd(IntEnum): "Lower Mountain": [], }, + "Monastery Back": { "Monastery Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], "Monastery Hero's Grave Region": [], }, @@ -1349,11 +1505,14 @@ class DeadEnd(IntEnum): "Monastery Back": [], }, + "Quarry Entry": { "Quarry Portal": [["Quarry Connector"]], "Quarry": [], + "Monastery Rope": + [["LS2"]], }, "Quarry Portal": { "Quarry Entry": @@ -1365,7 +1524,7 @@ class DeadEnd(IntEnum): "Quarry Back": [["Hyperdash"]], "Monastery Rope": - [["UR"]], + [["LS2"]], }, "Quarry Back": { "Quarry": @@ -1382,23 +1541,26 @@ class DeadEnd(IntEnum): [], "Quarry Monastery Entry": [], - "Lower Quarry Zig Door": - [["NMG"]], }, "Lower Quarry": { "Even Lower Quarry": [], }, "Even Lower Quarry": { - "Lower Quarry": + "Even Lower Quarry Isolated Chest": + [], + }, + "Even Lower Quarry Isolated Chest": { + "Even Lower Quarry": [], "Lower Quarry Zig Door": - [["Quarry", "Quarry Connector"], ["NMG"]], + [["Quarry", "Quarry Connector"], ["IG3"]], }, "Monastery Rope": { "Quarry Back": [], }, + "Rooted Ziggurat Upper Entry": { "Rooted Ziggurat Upper Front": [], @@ -1411,17 +1573,38 @@ class DeadEnd(IntEnum): "Rooted Ziggurat Upper Front": [["Hyperdash"]], }, + "Rooted Ziggurat Middle Top": { "Rooted Ziggurat Middle Bottom": [], }, + + "Rooted Ziggurat Lower Entry": { + "Rooted Ziggurat Lower Front": + [], + # can zip through to the checkpoint + "Rooted Ziggurat Lower Mid Checkpoint": + [["Hyperdash"]], + }, "Rooted Ziggurat Lower Front": { + "Rooted Ziggurat Lower Entry": + [], + "Rooted Ziggurat Lower Mid Checkpoint": + [], + }, + "Rooted Ziggurat Lower Mid Checkpoint": { + "Rooted Ziggurat Lower Entry": + [["Hyperdash"]], + "Rooted Ziggurat Lower Front": + [], "Rooted Ziggurat Lower Back": [], }, "Rooted Ziggurat Lower Back": { - "Rooted Ziggurat Lower Front": - [["Hyperdash"], ["UR"]], + "Rooted Ziggurat Lower Entry": + [["LS2"]], + "Rooted Ziggurat Lower Mid Checkpoint": + [["Hyperdash"], ["IG1"]], "Rooted Ziggurat Portal Room Entrance": [], }, @@ -1433,27 +1616,38 @@ class DeadEnd(IntEnum): "Rooted Ziggurat Lower Back": [], }, + "Rooted Ziggurat Portal Room Exit": { - "Rooted Ziggurat Portal": + "Rooted Ziggurat Portal Room": [], }, - "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room": { "Rooted Ziggurat Portal Room Exit": [["Rooted Ziggurat Lower Back"]], + "Rooted Ziggurat Portal": + [], + }, + "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": @@ -1467,24 +1661,53 @@ 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 Entry": { + "Cathedral to Gauntlet": + [], + "Cathedral Main": + [], + }, + "Cathedral Main": { + "Cathedral Entry": + [], + "Cathedral to Gauntlet": + [], + }, + "Cathedral to Gauntlet": { + "Cathedral Entry": + [], + "Cathedral Main": + [], + }, + "Cathedral Gauntlet Checkpoint": { "Cathedral Gauntlet": [], @@ -1497,6 +1720,7 @@ class DeadEnd(IntEnum): "Cathedral Gauntlet": [["Hyperdash"]], }, + "Far Shore": { "Far Shore to Spawn Region": [["Hyperdash"]], @@ -1507,7 +1731,7 @@ class DeadEnd(IntEnum): "Far Shore to Library Region": [["Library Lab"]], "Far Shore to West Garden Region": - [["West Garden"]], + [["West Garden South Checkpoint"]], "Far Shore to Fortress Region": [["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], }, diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index d9348628ce9c..163523108345 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1,8 +1,11 @@ -from typing import Dict, Set, List, Tuple, TYPE_CHECKING -from worlds.generic.Rules import set_rule, forbid_item -from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage -from .er_data import Portal -from .options import TunicOptions +from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING +from worlds.generic.Rules import set_rule, add_rule, forbid_item +from .options import IceGrappling, LadderStorage, CombatLogic +from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, + 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 .combat_logic import has_combat_reqs from BaseClasses import Region, CollectionState if TYPE_CHECKING: @@ -12,6 +15,7 @@ grapple = "Magic Orb" ice_dagger = "Magic Dagger" fire_wand = "Magic Wand" +gun = "Gun" lantern = "Lantern" fairies = "Fairy" coins = "Golden Coin" @@ -28,32 +32,61 @@ gold_hexagon = "Gold Questagon" -def has_ladder(ladder: str, state: CollectionState, player: int, options: TunicOptions): - return not options.shuffle_ladders or state.has(ladder, player) +def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool: + return not world.options.shuffle_ladders or state.has(ladder, world.player) -def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region], - portal_pairs: Dict[Portal, Portal]) -> None: +def can_shop(state: CollectionState, world: "TunicWorld") -> bool: + return has_sword(state, world.player) and state.can_reach_region("Shop", world.player) + + +def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None: player = world.player options = world.options + # input scene destination tag, returns portal's name and paired portal's outlet region or region + 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, get_portal_outlet_region(portal2, world) + if portal2.scene_destination() == portal_sd: + return portal2.name, get_portal_outlet_region(portal1, world) + raise Exception("No matches found in get_portal_info") + + # input scene destination tag, returns paired portal's name and region + def get_paired_portal(portal_sd: str) -> Tuple[str, str]: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene_destination() == portal_sd: + return portal2.name, portal2.region + if portal2.scene_destination() == portal_sd: + return portal1.name, portal1.region + raise Exception("no matches found in get_paired_portal") + regions["Menu"].connect( connecting_region=regions["Overworld"]) # Overworld regions["Overworld"].connect( connecting_region=regions["Overworld Holy Cross"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) # grapple on the west side, down the stairs from moss wall, across from ruined shop regions["Overworld"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has_any({laurels, grapple}, player)) + # regions["Overworld Beach"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) + # or state.has_any({laurels, grapple}, player)) + + # region for combat logic, no need to connect it to beach since it would be the same as the ow -> beach cxn + ow_tunnel_beach = regions["Overworld"].connect( + connecting_region=regions["Overworld Tunnel to Beach"]) + regions["Overworld Beach"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) - or state.has_any({laurels, grapple}, player)) + connecting_region=regions["Overworld Tunnel to Beach"], + rule=lambda state: state.has(laurels, player) or has_ladder("Ladders in Overworld Town", state, world)) regions["Overworld Beach"].connect( connecting_region=regions["Overworld West Garden Laurels Entry"], @@ -64,10 +97,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Beach"].connect( connecting_region=regions["Overworld to Atoll Upper"], - rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world)) regions["Overworld to Atoll Upper"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld to Atoll Upper"], @@ -78,83 +111,89 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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, player, options)) + 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, player, options)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld Belltower at Bell"], - rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + 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, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Weathervane", 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, player, options)) + 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, player, options) - or state.has(laurels, player)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) + 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, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) regions["After Ruined Passage"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Weathervane", 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, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) # 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, player, options, ability_unlocks)) + 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, player, options, ability_unlocks)) + 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, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", 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, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld at Patrol Cave"]) @@ -164,35 +203,35 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld at Patrol Cave"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", 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, player, options)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) or state.has(grapple, player)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", 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, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Upper Overworld"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", 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, player, options) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) or state.has(grapple, player)) regions["Upper Overworld"].connect( @@ -202,46 +241,46 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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, player, options)) + 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, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Quarry Entrance"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld above Quarry Entrance"], - rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], - rule=lambda state: state.has_any({laurels, grapple}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + rule=lambda state: state.has_any({laurels, grapple, gun}, player) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld"], - rule=lambda state: state.has_any({laurels, grapple}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + rule=lambda state: state.has_any({laurels, grapple, gun}, player) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + rule=lambda state: has_ladder("Ladder to Quarry", state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld after Envoy"], - rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + rule=lambda state: has_ladder("Ladder to Quarry", state, world)) # ice grapple through the gate regions["Overworld"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + 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, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], @@ -252,10 +291,11 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Lower Entry"], - rule=lambda state: has_ladder("Ladder to Swamp", state, player, options)) + 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, player, options)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld Special Shop Entry"], @@ -264,43 +304,50 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["East Overworld"], rule=lambda state: state.has(laurels, player)) - regions["Overworld"].connect( + # region made for combat logic + ow_to_well_entry = regions["Overworld"].connect( + connecting_region=regions["Overworld Well Entry Area"]) + regions["Overworld Well Entry Area"].connect( + connecting_region=regions["Overworld"]) + + regions["Overworld Well Entry Area"].connect( connecting_region=regions["Overworld Well Ladder"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Overworld Well Ladder"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + connecting_region=regions["Overworld Well Entry Area"], + rule=lambda state: has_ladder("Ladders in Well", state, world)) # nmg: can ice grapple through the door 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, player, options, ability_unlocks)) + 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(state, player, holy_cross, options, ability_unlocks)) + 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(state, player, holy_cross, options, ability_unlocks)) + 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(state, player, holy_cross, options, ability_unlocks)) + 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"]) - regions["Overworld"].connect( + ow_to_town_portal = regions["Overworld"].connect( connecting_region=regions["Overworld Town Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Overworld Town Portal"].connect( connecting_region=regions["Overworld"]) regions["Overworld"].connect( connecting_region=regions["Overworld Spawn Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Overworld Spawn Portal"].connect( connecting_region=regions["Overworld"]) @@ -308,7 +355,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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, player, options, ability_unlocks)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Temple Door"].connect( connecting_region=regions["Overworld above Patrol Cave"], @@ -316,28 +363,40 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has(grapple, player)) regions["Overworld Beach"].connect( connecting_region=regions["Overworld Tunnel Turret"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + # don't need the ice grapple rule since you can go from ow -> beach -> tunnel regions["Overworld"].connect( connecting_region=regions["Overworld Tunnel Turret"], - rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + 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)) + cube_entrance = regions["Overworld"].connect( + connecting_region=regions["Cube Cave Entrance Region"], + rule=lambda state: state.has(gun, player) or can_shop(state, world)) + world.multiworld.register_indirect_condition(regions["Shop"], cube_entrance) + 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"]) @@ -368,7 +427,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Hourglass Cave"].connect( connecting_region=regions["Hourglass Cave Tower"], - rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, player, options)) + rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, world)) # East Forest regions["Forest Belltower Upper"].connect( @@ -376,32 +435,31 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Forest Belltower Main"].connect( connecting_region=regions["Forest Belltower Lower"], - rule=lambda state: has_ladder("Ladder to East Forest", state, player, options)) + 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, player, options, ability_unlocks)) + 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, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Forest"].connect( connecting_region=regions["East Forest Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["East Forest Portal"].connect( connecting_region=regions["East Forest"]) regions["East Forest"].connect( connecting_region=regions["Lower Forest"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options) - or (state.has_all({grapple, fire_wand, ice_dagger}, player) # do ice slime, then go to the lower hook - and has_ability(state, player, icebolt, options, ability_unlocks))) + rule=lambda state: has_ladder("Ladders to Lower Forest", 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, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) regions["Guard House 1 East"].connect( connecting_region=regions["Guard House 1 West"]) @@ -411,133 +469,168 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Guard House 2 Upper"].connect( connecting_region=regions["Guard House 2 Lower"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) regions["Guard House 2 Lower"].connect( connecting_region=regions["Guard House 2 Upper"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + 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, player, options, ability_unlocks)) + 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, player, options, ability_unlocks) - 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"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Forest Hero's Grave"].connect( connecting_region=regions["Forest Grave Path by Grave"]) # Beneath the Well and Dark Tomb - # don't need the ladder when entering at the ladder spot regions["Beneath the Well Ladder Exit"].connect( connecting_region=regions["Beneath the Well Front"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Ladder Exit"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) - regions["Beneath the Well Front"].connect( + btw_front_main = regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Main"], - rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + rule=lambda state: has_melee(state, player) or state.has(fire_wand, player)) regions["Beneath the Well Main"].connect( - connecting_region=regions["Beneath the Well Front"], - rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + connecting_region=regions["Beneath the Well Front"]) regions["Beneath the Well Main"].connect( connecting_region=regions["Beneath the Well Back"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) - regions["Beneath the Well Back"].connect( + rule=lambda state: has_ladder("Ladders in Well", state, world)) + btw_back_main = regions["Beneath the Well Back"].connect( connecting_region=regions["Beneath the Well Main"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options) - and (has_stick(state, player) or state.has(fire_wand, player))) + rule=lambda state: has_ladder("Ladders in Well", state, world) + and (has_melee(state, player) or state.has(fire_wand, player))) - regions["Well Boss"].connect( + well_boss_to_dt = 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( + dt_entry_to_upper = regions["Dark Tomb Entry Point"].connect( connecting_region=regions["Dark Tomb Upper"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Entry Point"]) + # ice grapple through the wall, get the little secret sound to trigger regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Upper"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Dark Exit"]) - regions["Dark Tomb Dark Exit"].connect( + dt_exit_to_main = regions["Dark Tomb Dark Exit"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) # West Garden + # combat logic regions + wg_before_to_after_terry = regions["West Garden before Terry"].connect( + connecting_region=regions["West Garden after Terry"]) + wg_after_to_before_terry = regions["West Garden after Terry"].connect( + connecting_region=regions["West Garden before Terry"]) + + regions["West Garden after Terry"].connect( + connecting_region=regions["West Garden South Checkpoint"]) + wg_checkpoint_to_after_terry = regions["West Garden South Checkpoint"].connect( + connecting_region=regions["West Garden after Terry"]) + + wg_checkpoint_to_dagger = regions["West Garden South Checkpoint"].connect( + connecting_region=regions["West Garden at Dagger House"]) + regions["West Garden at Dagger House"].connect( + connecting_region=regions["West Garden South Checkpoint"]) + + wg_checkpoint_to_before_boss = regions["West Garden South Checkpoint"].connect( + connecting_region=regions["West Garden before Boss"]) + regions["West Garden before Boss"].connect( + connecting_region=regions["West Garden South Checkpoint"]) + regions["West Garden Laurels Exit Region"].connect( - connecting_region=regions["West Garden"], + connecting_region=regions["West Garden at Dagger House"], rule=lambda state: state.has(laurels, player)) - regions["West Garden"].connect( + regions["West Garden at Dagger House"].connect( connecting_region=regions["West Garden Laurels Exit Region"], rule=lambda state: state.has(laurels, player)) - regions["West Garden after Boss"].connect( - connecting_region=regions["West Garden"], - rule=lambda state: state.has(laurels, player)) - regions["West Garden"].connect( + # laurels past, or ice grapple it off, or ice grapple to it then fight + after_gk_to_wg = regions["West Garden after Boss"].connect( + connecting_region=regions["West Garden before Boss"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or (has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + and has_sword(state, player))) + # ice grapple push Garden Knight off the side + wg_to_after_gk = regions["West Garden before Boss"].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( + regions["West Garden before Terry"].connect( connecting_region=regions["West Garden Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["West Garden Hero's Grave Region"].connect( - connecting_region=regions["West Garden"]) + connecting_region=regions["West Garden before Terry"]) 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(state, player, prayer, options, ability_unlocks)) + 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, player, options, ability_unlocks)) - regions["West Garden"].connect( + connecting_region=regions["West Garden at Dagger House"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + regions["West Garden at Dagger House"].connect( connecting_region=regions["West Garden Portal Item"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + 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, player, options, ability_unlocks)) + 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)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Ladder Tops"], - rule=lambda state: has_ladder("Ladders in South Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladders in South Atoll", state, world)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Frog Mouth"], @@ -548,122 +641,139 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Frog Eye"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Ruined Atoll Frog Eye"].connect( connecting_region=regions["Ruined Atoll"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Ruined Atoll Portal"].connect( connecting_region=regions["Ruined Atoll"]) - regions["Ruined Atoll"].connect( + atoll_statue = regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and has_ladder("Ladders in South Atoll", state, player, options)) + rule=lambda state: has_ability(prayer, 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, player, options)) + 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, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Upper"].connect( connecting_region=regions["Frog Stairs Lower"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Lower"].connect( connecting_region=regions["Frog Stairs Upper"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Lower"].connect( connecting_region=regions["Frog Stairs to Frog's Domain"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs to Frog's Domain"].connect( connecting_region=regions["Frog Stairs Lower"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog's Domain Entry"].connect( - connecting_region=regions["Frog's Domain"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + connecting_region=regions["Frog's Domain Front"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) - regions["Frog's Domain"].connect( + frogs_front_to_main = regions["Frog's Domain Front"].connect( + connecting_region=regions["Frog's Domain Main"]) + + regions["Frog's Domain Main"].connect( connecting_region=regions["Frog's Domain Back"], rule=lambda state: state.has(grapple, player)) # 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, player, options)) + 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(state, player, prayer, options, ability_unlocks) - and (state.has(grapple, player) or (state.has(laurels, player) - and has_ladder("Ladders in Library", state, player, options)))) + 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"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hall Bookshelf"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Library Hero's Grave Region"].connect( connecting_region=regions["Library Hall"]) regions["Library Hall to Rotunda"].connect( connecting_region=regions["Library Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hall to Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda to Hall"].connect( connecting_region=regions["Library Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda"].connect( connecting_region=regions["Library Rotunda to Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda"].connect( connecting_region=regions["Library Rotunda to Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda to Lab"].connect( connecting_region=regions["Library Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Lab Lower"].connect( connecting_region=regions["Library Lab"], rule=lambda state: state.has_any({grapple, laurels}, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( connecting_region=regions["Library Lab Lower"], rule=lambda state: state.has(laurels, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( - connecting_region=regions["Library Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and has_ladder("Ladders in Library", state, player, options)) - 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, player, options) + 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, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Lab to Librarian"].connect( connecting_region=regions["Library Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) # Eastern Vault Fortress regions["Fortress Exterior from East Forest"].connect( @@ -678,14 +788,19 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player)) regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Exterior near cave"], - rule=lambda state: state.has(laurels, player) or has_ability(state, player, prayer, options, ability_unlocks)) + 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, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Beneath the Vault Entry"].connect( connecting_region=regions["Fortress Exterior near cave"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Fortress Courtyard"].connect( connecting_region=regions["Fortress Exterior from Overworld"], @@ -694,66 +809,76 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - regions["Fortress Courtyard Upper"].connect( + fort_upper_lower = 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, player, options, ability_unlocks)) + 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"]) - regions["Beneath the Vault Ladder Exit"].connect( + btv_front_to_main = 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, player, options) - and has_lantern(state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) + and has_lantern(state, world) + # there's some boxes in the way + and (has_melee(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, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Back"]) - regions["Beneath the Vault Back"].connect( + btv_back_to_main = regions["Beneath the Vault Back"].connect( connecting_region=regions["Beneath the Vault Main"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) - regions["Fortress East Shortcut Upper"].connect( + fort_east_upper_lower = regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) - # 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, player, options, ability_unlocks)) + 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, player, options, ability_unlocks)) + 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, player, options, ability_unlocks)) + 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"], - rule=lambda state: state.has(laurels, player)) - regions["Fortress Grave Path Dusty Entrance Region"].connect( - connecting_region=regions["Fortress Grave Path"], - rule=lambda state: state.has(laurels, player)) + fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect( + connecting_region=regions["Fortress Grave Path Combat"]) + regions["Fortress Grave Path Combat"].connect( + connecting_region=regions["Fortress Grave Path Entry"]) + + regions["Fortress Grave Path Combat"].connect( + connecting_region=regions["Fortress Grave Path by Grave"]) + + # run past the enemies + regions["Fortress Grave Path by Grave"].connect( + connecting_region=regions["Fortress Grave Path Entry"]) - regions["Fortress Grave Path"].connect( + regions["Fortress Grave Path by Grave"].connect( connecting_region=regions["Fortress Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Fortress Hero's Grave Region"].connect( - connecting_region=regions["Fortress Grave Path"]) + connecting_region=regions["Fortress Grave Path by Grave"]) + + regions["Fortress Grave Path by Grave"].connect( + connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], + rule=lambda state: state.has(laurels, player)) + # reverse connection is conditionally made later, depending on whether combat logic is on, and the details of ER - # 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, player, options, ability_unlocks)) + connecting_region=regions["Fortress Grave Path Entry"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], @@ -764,10 +889,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # Quarry regions["Lower Mountain"].connect( connecting_region=regions["Lower Mountain Stairs"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Lower Mountain Stairs"].connect( connecting_region=regions["Lower Mountain"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Quarry Entry"].connect( connecting_region=regions["Quarry Portal"], @@ -775,19 +900,19 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Quarry Portal"].connect( connecting_region=regions["Quarry Entry"]) - regions["Quarry Entry"].connect( + quarry_entry_to_main = regions["Quarry Entry"].connect( connecting_region=regions["Quarry"], rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) regions["Quarry"].connect( connecting_region=regions["Quarry Entry"]) - regions["Quarry Back"].connect( + quarry_back_to_main = regions["Quarry Back"].connect( connecting_region=regions["Quarry"], rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) regions["Quarry"].connect( connecting_region=regions["Quarry Back"]) - regions["Quarry Monastery Entry"].connect( + monastery_to_quarry_main = regions["Quarry Monastery Entry"].connect( connecting_region=regions["Quarry"], rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) regions["Quarry"].connect( @@ -805,36 +930,41 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Quarry"].connect( connecting_region=regions["Lower Quarry"], - rule=lambda state: has_mask(state, player, options)) + rule=lambda state: has_mask(state, world)) # need the ladder, or you can ice grapple down in nmg regions["Lower Quarry"].connect( connecting_region=regions["Even Lower Quarry"], - rule=lambda state: has_ladder("Ladders in Lower Quarry", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders in Lower Quarry", 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["Even Lower Quarry Isolated Chest"]) + # you grappled down, might as well loot the rest too + lower_quarry_empty_to_combat = regions["Even Lower Quarry Isolated Chest"].connect( + connecting_region=regions["Even Lower Quarry"], + rule=lambda state: has_mask(state, world)) + + regions["Even Lower Quarry Isolated Chest"].connect( connecting_region=regions["Lower Quarry Zig Door"], rule=lambda state: state.has("Activate Quarry Fuse", player) - or (has_ice_grapple_logic(False, state, player, options, ability_unlocks) 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 + # don't need the mask for this either, please don't complain about not needing a mask here, you know what you did regions["Quarry"].connect( - connecting_region=regions["Lower Quarry Zig Door"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and options.entrance_rando) + connecting_region=regions["Even Lower Quarry Isolated Chest"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) - regions["Monastery Front"].connect( + monastery_front_to_back = 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"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Monastery Hero's Grave Region"].connect( connecting_region=regions["Monastery Back"]) @@ -842,7 +972,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Rooted Ziggurat Upper Entry"].connect( connecting_region=regions["Rooted Ziggurat Upper Front"]) - regions["Rooted Ziggurat Upper Front"].connect( + zig_upper_front_back = regions["Rooted Ziggurat Upper Front"].connect( connecting_region=regions["Rooted Ziggurat Upper Back"], rule=lambda state: state.has(laurels, player) or has_sword(state, player)) regions["Rooted Ziggurat Upper Back"].connect( @@ -852,69 +982,101 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Rooted Ziggurat Middle Top"].connect( connecting_region=regions["Rooted Ziggurat Middle Bottom"]) + zig_low_entry_to_front = regions["Rooted Ziggurat Lower Entry"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"]) + regions["Rooted Ziggurat Lower Front"].connect( + connecting_region=regions["Rooted Ziggurat Lower Entry"]) + regions["Rooted Ziggurat Lower Front"].connect( + connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"]) + zig_low_mid_to_front = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"]) + + zig_low_mid_to_back = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"], rule=lambda state: state.has(laurels, player) - or (has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks))) - # 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, player, options, ability_unlocks)) - and has_ability(state, player, prayer, options, ability_unlocks) - and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or (has_sword(state, player) and has_ability(prayer, state, world))) + # can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse + zig_low_back_to_mid = regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"], + 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"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Rooted Ziggurat Portal Room Entrance"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"]) - regions["Zig Skip Exit"].connect( - connecting_region=regions["Rooted Ziggurat Lower Front"]) + # zig skip region only gets made if entrance rando and fewer shops are on + if options.entrance_rando and options.fixed_shop: + regions["Zig Skip Exit"].connect( + 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(state, player, prayer, options, ability_unlocks)) + 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, player, options) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # 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, player, options) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # 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( + swamp_mid_to_cath = regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], - rule=lambda state: (has_ability(state, player, prayer, options, ability_unlocks) - and state.has(laurels, player)) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + 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))))) + and (not options.combat_logic + or has_combat_reqs("Swamp", state, 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, player, options, ability_unlocks)) + 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, player, options)) + 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, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) # nmg: ice grapple the enemy at door + rule=lambda state: has_ladder("Ladders in Swamp", state, world) + 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"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Swamp to Cathedral Treasure Room"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"]) @@ -925,18 +1087,42 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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, player, options, ability_unlocks)) + 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"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) + cath_entry_to_elev = regions["Cathedral Entry"].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 Entry"]) + + cath_entry_to_main = regions["Cathedral Entry"].connect( + connecting_region=regions["Cathedral Main"]) + regions["Cathedral Main"].connect( + connecting_region=regions["Cathedral Entry"]) + + cath_elev_to_main = regions["Cathedral to Gauntlet"].connect( + connecting_region=regions["Cathedral Main"]) + regions["Cathedral Main"].connect( + connecting_region=regions["Cathedral to Gauntlet"]) + regions["Cathedral Gauntlet Checkpoint"].connect( connecting_region=regions["Cathedral Gauntlet"]) @@ -987,554 +1173,715 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Far Shore"]) # Misc - regions["Spirit Arena"].connect( + heir_fight = regions["Spirit Arena"].connect( connecting_region=regions["Spirit Arena Victory"], rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if world.options.hexagon_quest else - (state.has_all({red_hexagon, green_hexagon, blue_hexagon, "Unseal the Heir"}, player) - and state.has_group_unique("Hero Relics", player, 6)))) + (state.has("Unseal the Heir", player) + 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(state, player, holy_cross, options, ability_unlocks) - and (has_ladder("Ladders in Swamp", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks) - 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, player, options)) - 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, player, options, ability_unlocks))) - 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(state, player, prayer, options, ability_unlocks)) - # 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) + + # convenient staircase means this one is easy difficulty, even though there's an elevation change + ls_connect("LS Elev 0", "Overworld Redux, Furnace_gyro_west") + + # 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 2", "Overworld Redux, Ruins Passage_west") + 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, player, options, ability_unlocks))))) - # 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 - 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)) - -def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: + 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[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) + + # for combat logic, easiest to replace or add to existing rules + if world.options.combat_logic >= CombatLogic.option_bosses_only: + set_rule(wg_to_after_gk, + lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or has_combat_reqs("Garden Knight", state, player)) + # laurels past, or ice grapple it off, or ice grapple to it and fight + set_rule(after_gk_to_wg, + lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or (has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + and has_combat_reqs("Garden Knight", state, player))) + + if not world.options.hexagon_quest: + add_rule(heir_fight, + lambda state: has_combat_reqs("The Heir", state, player)) + + if world.options.combat_logic == CombatLogic.option_on: + # these are redundant with combat logic off + regions["Fortress Grave Path Entry"].connect( + connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], + rule=lambda state: state.has(laurels, player)) + + regions["Rooted Ziggurat Lower Entry"].connect( + connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"], + rule=lambda state: state.has(laurels, player)) + regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( + connecting_region=regions["Rooted Ziggurat Lower Entry"], + rule=lambda state: state.has(laurels, player)) + + add_rule(ow_to_town_portal, + lambda state: has_combat_reqs("Before Well", state, player)) + # need to fight through the rudelings and turret, or just laurels from near the windmill + set_rule(ow_to_well_entry, + lambda state: state.has(laurels, player) + or has_combat_reqs("East Forest", state, player)) + set_rule(ow_tunnel_beach, + lambda state: has_combat_reqs("East Forest", state, player)) + + add_rule(atoll_statue, + lambda state: has_combat_reqs("Ruined Atoll", state, player)) + set_rule(frogs_front_to_main, + lambda state: has_combat_reqs("Frog's Domain", state, player)) + + set_rule(btw_front_main, + lambda state: state.has(laurels, player) or has_combat_reqs("Beneath the Well", state, player)) + set_rule(btw_back_main, + lambda state: has_ladder("Ladders in Well", state, world) + and (state.has(laurels, player) or has_combat_reqs("Beneath the Well", state, player))) + set_rule(well_boss_to_dt, + lambda state: has_combat_reqs("Beneath the Well", state, player) + or laurels_zip(state, world)) + + add_rule(dt_entry_to_upper, + lambda state: has_combat_reqs("Dark Tomb", state, player)) + add_rule(dt_exit_to_main, + lambda state: has_combat_reqs("Dark Tomb", state, player)) + + set_rule(wg_before_to_after_terry, + lambda state: state.has_any({laurels, ice_dagger}, player) + or has_combat_reqs("West Garden", state, player)) + set_rule(wg_after_to_before_terry, + lambda state: state.has_any({laurels, ice_dagger}, player) + or has_combat_reqs("West Garden", state, player)) + # laurels through, probably to the checkpoint, or just fight + set_rule(wg_checkpoint_to_after_terry, + lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player)) + set_rule(wg_checkpoint_to_before_boss, + lambda state: has_combat_reqs("West Garden", state, player)) + + add_rule(btv_front_to_main, + lambda state: has_combat_reqs("Beneath the Vault", state, player)) + add_rule(btv_back_to_main, + lambda state: has_combat_reqs("Beneath the Vault", state, player)) + + add_rule(fort_upper_lower, + lambda state: state.has(ice_dagger, player) + or has_combat_reqs("Eastern Vault Fortress", state, player)) + set_rule(fort_grave_entry_to_combat, + lambda state: has_combat_reqs("Eastern Vault Fortress", state, player)) + + set_rule(quarry_entry_to_main, + lambda state: has_combat_reqs("Quarry", state, player)) + set_rule(quarry_back_to_main, + lambda state: has_combat_reqs("Quarry", state, player)) + set_rule(monastery_to_quarry_main, + lambda state: has_combat_reqs("Quarry", state, player)) + set_rule(monastery_front_to_back, + lambda state: has_combat_reqs("Quarry", state, player)) + set_rule(lower_quarry_empty_to_combat, + lambda state: has_combat_reqs("Quarry", state, player)) + + set_rule(zig_upper_front_back, + lambda state: state.has(laurels, player) + or has_combat_reqs("Rooted Ziggurat", state, player)) + set_rule(zig_low_entry_to_front, + lambda state: has_combat_reqs("Rooted Ziggurat", state, player)) + set_rule(zig_low_mid_to_front, + lambda state: has_combat_reqs("Rooted Ziggurat", state, player)) + set_rule(zig_low_mid_to_back, + lambda state: state.has(laurels, player) + or (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))) + set_rule(zig_low_back_to_mid, + 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_combat_reqs("Rooted Ziggurat", state, player)) + + # only activating the fuse requires combat logic + set_rule(cath_entry_to_elev, + lambda state: options.entrance_rando + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or (has_ability(prayer, state, world) and has_combat_reqs("Cathedral", state, player))) + + set_rule(cath_entry_to_main, + lambda state: has_combat_reqs("Cathedral", state, player)) + set_rule(cath_elev_to_main, + lambda state: has_combat_reqs("Cathedral", state, player)) + + # for spots where you can go into and come out of an entrance to reset enemy aggro + if world.options.entrance_rando: + # for the chest outside of magic dagger house + dagger_entry_paired_name, dagger_entry_paired_region = ( + get_paired_portal("Archipelagos Redux, archipelagos_house_")) + try: + dagger_entry_paired_entrance = world.get_entrance(dagger_entry_paired_name) + except KeyError: + # there is no paired entrance, so you must fight or dash past, which is done in the finally + pass + else: + set_rule(wg_checkpoint_to_dagger, + lambda state: dagger_entry_paired_entrance.can_reach(state)) + world.multiworld.register_indirect_condition(region=regions["West Garden at Dagger House"], + entrance=dagger_entry_paired_entrance) + finally: + add_rule(wg_checkpoint_to_dagger, + lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player), + combine="or") + + # zip past enemies in fortress grave path to enter the dusty entrance, then come back out + fort_dusty_paired_name, fort_dusty_paired_region = get_paired_portal("Fortress Reliquary, Dusty_") + try: + fort_dusty_paired_entrance = world.get_entrance(fort_dusty_paired_name) + except KeyError: + # there is no paired entrance, so you can't run past to deaggro + # the path to dusty can be done via combat, so no need to do anything here + pass + else: + # there is a paired entrance, so you can use that to deaggro enemies + regions["Fortress Grave Path Dusty Entrance Region"].connect( + connecting_region=regions["Fortress Grave Path by Grave"], + rule=lambda state: state.has(laurels, player) and fort_dusty_paired_entrance.can_reach(state)) + world.multiworld.register_indirect_condition(region=regions["Fortress Grave Path by Grave"], + entrance=fort_dusty_paired_entrance) + + # for activating the ladder switch to get from fortress east upper to lower + fort_east_upper_right_paired_name, fort_east_upper_right_paired_region = ( + get_paired_portal("Fortress East, Fortress Courtyard_")) + try: + fort_east_upper_right_paired_entrance = ( + world.get_entrance(fort_east_upper_right_paired_name)) + except KeyError: + # no paired entrance, so you must fight, which is done in the finally + pass + else: + set_rule(fort_east_upper_lower, + lambda state: fort_east_upper_right_paired_entrance.can_reach(state)) + world.multiworld.register_indirect_condition(region=regions["Fortress East Shortcut Lower"], + entrance=fort_east_upper_right_paired_entrance) + finally: + add_rule(fort_east_upper_lower, + lambda state: has_combat_reqs("Eastern Vault Fortress", state, player) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world), + combine="or") + + else: + # if combat logic is on and ER is off, we can make this entrance freely + regions["Fortress Grave Path Dusty Entrance Region"].connect( + connecting_region=regions["Fortress Grave Path by Grave"], + rule=lambda state: state.has(laurels, player)) + else: + # if combat logic is off, we can make this entrance freely + regions["Fortress Grave Path Dusty Entrance Region"].connect( + connecting_region=regions["Fortress Grave Path by Grave"], + rule=lambda state: state.has(laurels, player)) + + +def set_er_location_rules(world: "TunicWorld") -> None: player = world.player - multiworld = world.multiworld - options = world.options - forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + + forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) # Ability Shuffle Exclusive Rules - set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + 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"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), lambda state: state.has("Activate Furnace Fuse", player)) - set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Overworld - [Southwest] Flowers Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Overworld - [East] Weathervane Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Overworld - [Northeast] Flowers Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Overworld - [Southwest] Haiku Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Overworld - [Northwest] Golden Obelisk Page", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Library Hall - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Overworld - [Southwest] Flowers Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Overworld - [East] Weathervane Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Overworld - [Northeast] Flowers Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Overworld - [Southwest] Haiku Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Overworld - [Northwest] Golden Obelisk Page"), + lambda state: has_ability(holy_cross, state, world)) # Overworld - set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + set_rule(world.get_location("Overworld - [Southwest] From West Garden"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + set_rule(world.get_location("Overworld - [Southwest] Fountain Page"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + set_rule(world.get_location("Old House - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + set_rule(world.get_location("Sealed Temple - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Caustic Light Cave - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Cube Cave - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Old House - Holy Cross Door Page"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Maze Cave - Maze Room Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Old House - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Patrol Cave - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Ruined Passage - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Hourglass Cave - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Secret Gathering Place - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"), lambda state: state.has(fairies, player, 10)) - set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), lambda state: state.has(coins, player, 6)) - set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + set_rule(world.get_location("Coins in the Well - 3 Coins"), + lambda state: state.has(coins, player, 3)) + set_rule(world.get_location("Coins in the Well - 6 Coins"), + lambda state: state.has(coins, player, 6)) + set_rule(world.get_location("Coins in the Well - 10 Coins"), lambda state: state.has(coins, player, 10)) - set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + set_rule(world.get_location("Coins in the Well - 15 Coins"), lambda state: state.has(coins, player, 15)) # East Forest - set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + set_rule(world.get_location("East Forest - Lower Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + set_rule(world.get_location("East Forest - Lower Dash Chest"), lambda state: state.has_all({grapple, laurels}, player)) - set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: ( - state.has_all({grapple, ice_dagger, fire_wand}, player) and - has_ability(state, player, icebolt, options, ability_unlocks))) + set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: ( + state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world))) + + # Dark Tomb + # added to make combat logic smoother + set_rule(world.get_location("Dark Tomb - 2nd Laser Room"), + lambda state: has_lantern(state, world)) # West Garden - set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + set_rule(world.get_location("West Garden - [West] In Flooded Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), - lambda state: state.has(laurels, player) and has_ability(state, player, holy_cross, options, - ability_unlocks)) - set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), + set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), + 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)) - set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) # Ruined Atoll - set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + # 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) + 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) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Frog's Domain - set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + 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_melee(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress - set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + 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_melee(state, player) or state.has(ice_dagger, player)) # Beneath the Vault - set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), - lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) + set_rule(world.get_location("Beneath the Fortress - Bridge"), + lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player)) # Quarry - set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + 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 - set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), - lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player) - or options.entrance_rando))) - set_rule(multiworld.get_location("Rooted Ziggurat Lower - After Guarded Fuse", player), - lambda state: has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks)) + # 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 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(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + 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(multiworld.get_location("Librarian - Hexagon Green", player), - lambda state: (has_sword(state, player) or options.logic_rules) - and has_ladder("Ladders in Library", state, player, options)) - # nmg - kill boss scav with orb + firecracker, or similar - set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + set_rule(world.get_location("Librarian - Hexagon Green"), + lambda state: has_sword(state, player) + and has_ladder("Ladders in Library", state, world)) + # 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 has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Swamp - set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: state.has(fire_wand, player) and has_sword(state, player)) - set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", 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 - set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + # 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)) # Hero's Grave and Far Shore - set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + set_rule(world.get_location("Hero's Grave - Tooth Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + set_rule(world.get_location("Hero's Grave - Mushroom Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + set_rule(world.get_location("Hero's Grave - Ash Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + set_rule(world.get_location("Hero's Grave - Flowers Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + set_rule(world.get_location("Hero's Grave - Effigy Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + set_rule(world.get_location("Hero's Grave - Feathers Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + set_rule(world.get_location("Far Shore - Secret Chest"), lambda state: state.has(laurels, player)) # Events - set_rule(multiworld.get_location("Eastern Bell", player), - lambda state: (has_stick(state, player) or state.has(fire_wand, player))) - set_rule(multiworld.get_location("Western Bell", player), - lambda state: (has_stick(state, player) or state.has(fire_wand, player))) - set_rule(multiworld.get_location("Furnace Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Beneath the Vault Fuse", player), + set_rule(world.get_location("Eastern Bell"), + lambda state: (has_melee(state, player) or state.has(fire_wand, player))) + set_rule(world.get_location("Western Bell"), + lambda state: (has_melee(state, player) or state.has(fire_wand, player))) + set_rule(world.get_location("Furnace Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("South and West Fortress Exterior Fuses"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Upper and Central Fortress Exterior Fuses"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Beneath the Vault Fuse"), lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) - set_rule(multiworld.get_location("Eastern Vault West Fuses", player), + set_rule(world.get_location("Eastern Vault West Fuses"), lambda state: state.has("Activate Beneath the Vault Fuse", player)) - set_rule(multiworld.get_location("Eastern Vault East Fuse", player), + set_rule(world.get_location("Eastern Vault East Fuse"), lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses", "Activate South and West Fortress Exterior Fuses"}, player)) - set_rule(multiworld.get_location("Quarry Connector Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) and state.has(grapple, player)) - set_rule(multiworld.get_location("Quarry Fuse", player), + set_rule(world.get_location("Quarry Connector Fuse"), + lambda state: has_ability(prayer, state, world) and state.has(grapple, player)) + set_rule(world.get_location("Quarry Fuse"), lambda state: state.has("Activate Quarry Connector Fuse", player)) - set_rule(multiworld.get_location("Ziggurat Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("West Garden Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Library Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(world.get_location("Ziggurat Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("West Garden Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Library Fuse"), + lambda state: has_ability(prayer, state, world) and has_ladder("Ladders in Library", state, world)) + if not world.options.hexagon_quest: + set_rule(world.get_location("Place Questagons"), + lambda state: state.has_all((red_hexagon, blue_hexagon, green_hexagon), player)) + + # 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) + 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"), + lambda state: has_sword(state, player)) + set_rule(world.get_location("Shop - Potion 2"), + lambda state: has_sword(state, player)) + set_rule(world.get_location("Shop - Coin 1"), + lambda state: has_sword(state, player)) + set_rule(world.get_location("Shop - Coin 2"), + lambda state: has_sword(state, player)) + + def combat_logic_to_loc(loc_name: str, combat_req_area: str, set_instead: bool = False, + dagger: bool = False, laurel: bool = False) -> None: + # dagger means you can use magic dagger instead of combat for that check + # laurel means you can dodge the enemies freely with the laurels + if set_instead: + set_rule(world.get_location(loc_name), + lambda state: has_combat_reqs(combat_req_area, state, player) + or (dagger and state.has(ice_dagger, player)) + or (laurel and state.has(laurels, player))) + else: + add_rule(world.get_location(loc_name), + lambda state: has_combat_reqs(combat_req_area, state, player) + or (dagger and state.has(ice_dagger, player)) + or (laurel and state.has(laurels, player))) + + if world.options.combat_logic >= CombatLogic.option_bosses_only: + # garden knight is in the regions part above + combat_logic_to_loc("Fortress Arena - Siege Engine/Vault Key Pickup", "Siege Engine", set_instead=True) + combat_logic_to_loc("Librarian - Hexagon Green", "The Librarian", set_instead=True) + set_rule(world.get_location("Librarian - Hexagon Green"), + rule=lambda state: has_combat_reqs("The Librarian", state, player) + and has_ladder("Ladders in Library", state, world)) + combat_logic_to_loc("Rooted Ziggurat Lower - Hexagon Blue", "Boss Scavenger", set_instead=True) + if world.options.ice_grappling >= IceGrappling.option_medium: + add_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), + lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + combat_logic_to_loc("Cathedral Gauntlet - Gauntlet Reward", "Gauntlet", set_instead=True) + + if world.options.combat_logic == CombatLogic.option_on: + combat_logic_to_loc("Overworld - [Northeast] Flowers Holy Cross", "Garden Knight") + combat_logic_to_loc("Overworld - [Northwest] Chest Near Quarry Gate", "Before Well", dagger=True) + combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "Garden Knight", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld") + combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "East Forest", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "East Forest", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "East Forest", dagger=True) + combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "East Forest", dagger=True) + combat_logic_to_loc("Overworld - [Northwest] Chest Near Turret", "Before Well") + + add_rule(world.get_location("Hourglass Cave - Hourglass Chest"), + lambda state: has_sword(state, player) and (state.has("Shield", player) + # kill the turrets through the wall with a longer sword + or state.has("Sword Upgrade", player, 3))) + add_rule(world.get_location("Hourglass Cave - Holy Cross Chest"), + lambda state: has_sword(state, player) and (state.has("Shield", player) + or state.has("Sword Upgrade", player, 3))) + + # the first spider chest they literally do not attack you until you open the chest + # the second one, you can still just walk past them, but I guess /something/ would be wanted + combat_logic_to_loc("East Forest - Beneath Spider Chest", "East Forest", dagger=True, laurel=True) + combat_logic_to_loc("East Forest - Golden Obelisk Holy Cross", "East Forest", dagger=True) + combat_logic_to_loc("East Forest - Dancing Fox Spirit Holy Cross", "East Forest", dagger=True, laurel=True) + combat_logic_to_loc("East Forest - From Guardhouse 1 Chest", "East Forest", dagger=True, laurel=True) + combat_logic_to_loc("East Forest - Above Save Point", "East Forest", dagger=True) + combat_logic_to_loc("East Forest - Above Save Point Obscured", "East Forest", dagger=True) + combat_logic_to_loc("Forest Grave Path - Above Gate", "East Forest", dagger=True, laurel=True) + combat_logic_to_loc("Forest Grave Path - Obscured Chest", "East Forest", dagger=True, laurel=True) + + # most of beneath the well is covered by the region access rule + combat_logic_to_loc("Beneath the Well - [Entryway] Chest", "Beneath the Well") + combat_logic_to_loc("Beneath the Well - [Entryway] Obscured Behind Waterfall", "Beneath the Well") + combat_logic_to_loc("Beneath the Well - [Back Corridor] Left Secret", "Beneath the Well") + combat_logic_to_loc("Beneath the Well - [Side Room] Chest By Phrends", "Overworld") + + # laurels past the enemies, then use the wand or gun to take care of the fairies that chased you + add_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), + lambda state: state.has_any({fire_wand, "Gun"}, player)) + combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Faeries", "West Garden") + combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Save Point", "West Garden") + combat_logic_to_loc("West Garden - [West Highlands] Upper Left Walkway", "West Garden") + + # with combat logic on, I presume the player will want to be able to see to avoid the spiders + set_rule(world.get_location("Beneath the Fortress - Bridge"), + lambda state: has_lantern(state, world) + and (state.has_any({laurels, fire_wand, "Gun"}, player) or has_melee(state, player))) + + combat_logic_to_loc("Eastern Vault Fortress - [West Wing] Candles Holy Cross", "Eastern Vault Fortress", + dagger=True) + + # could just do the last two, but this outputs better in the spoiler log + # dagger is maybe viable here, but it's sketchy -- activate ladder switch, save to reset enemies, climb up + combat_logic_to_loc("Upper and Central Fortress Exterior Fuses", "Eastern Vault Fortress") + combat_logic_to_loc("Beneath the Vault Fuse", "Beneath the Vault") + combat_logic_to_loc("Eastern Vault West Fuses", "Eastern Vault Fortress") + + # if you come in from the left, you only need to fight small crabs + add_rule(world.get_location("Ruined Atoll - [South] Near Birds"), + lambda state: has_melee(state, player) or state.has_any({laurels, "Gun"}, player)) + + # can get this one without fighting if you have laurels + add_rule(world.get_location("Frog's Domain - Above Vault"), + lambda state: state.has(laurels, player) or has_combat_reqs("Frog's Domain", state, player)) + + # with wand, you can get this chest. Non-ER, you need laurels to continue down. ER, you can just torch + set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), + lambda state: (state.has(fire_wand, player) + and (state.has(laurels, player) or world.options.entrance_rando)) + or has_combat_reqs("Rooted Ziggurat", state, player)) + set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), + lambda state: has_ability(prayer, state, world) + and has_combat_reqs("Rooted Ziggurat", state, player)) + + # replace the sword rule with this one + combat_logic_to_loc("Swamp - [South Graveyard] 4 Orange Skulls", "Swamp", set_instead=True) + combat_logic_to_loc("Swamp - [South Graveyard] Guarded By Big Skeleton", "Swamp", dagger=True) + # don't really agree with this one but eh + combat_logic_to_loc("Swamp - [South Graveyard] Above Big Skeleton", "Swamp", dagger=True, laurel=True) + # the tentacles deal with everything else reasonably, and you can hide on the island, so no rule for it + add_rule(world.get_location("Swamp - [South Graveyard] Obscured Beneath Telescope"), + lambda state: state.has(laurels, player) # can dash from swamp mid to here and grab it + or has_combat_reqs("Swamp", state, player)) + add_rule(world.get_location("Swamp - [Central] South Secret Passage"), + lambda state: state.has(laurels, player) # can dash from swamp front to here and grab it + or has_combat_reqs("Swamp", state, player)) + combat_logic_to_loc("Swamp - [South Graveyard] Upper Walkway On Pedestal", "Swamp") + combat_logic_to_loc("Swamp - [Central] Beneath Memorial", "Swamp") + combat_logic_to_loc("Swamp - [Central] Near Ramps Up", "Swamp") + combat_logic_to_loc("Swamp - [Upper Graveyard] Near Telescope", "Swamp") + combat_logic_to_loc("Swamp - [Upper Graveyard] Near Shield Fleemers", "Swamp") + combat_logic_to_loc("Swamp - [Upper Graveyard] Obscured Behind Hill", "Swamp") + + # zip through the rubble to sneakily grab this chest, or just fight to it + add_rule(world.get_location("Cathedral - [1F] Near Spikes"), + lambda state: laurels_zip(state, world) or has_combat_reqs("Cathedral", state, player)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 9d25137ba469..aa5833b4db36 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,26 +22,41 @@ class TunicERLocation(Location): def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} + if world.options.entrance_rando: - portal_pairs = pair_portals(world) + for region_name, region_data in world.er_regions.items(): + # if fewer shops is off, zig skip is not made + if region_name == "Zig Skip Exit": + # need to check if there's a seed group for this first + if world.options.entrance_rando.value not in EntranceRando.options.values(): + if not world.seed_groups[world.options.entrance_rando.value]["fixed_shop"]: + continue + elif not world.options.fixed_shop: + continue + regions[region_name] = Region(region_name, world.player, world.multiworld) + + portal_pairs = pair_portals(world, regions) # output the entrances to the spoiler log here for convenience - for portal1, portal2 in portal_pairs.items(): - world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) + 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 world.er_regions.items(): + # filter out regions that are inaccessible in non-er + if region_name not in ["Zig Skip Exit", "Purgatory"]: + regions[region_name] = Region(region_name, world.player, world.multiworld) - 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, world.ability_unlocks, regions, portal_pairs) + create_randomized_entrances(portal_pairs, regions) + + set_er_region_rules(world, regions, portal_pairs) for location_name, location_id in world.location_name_to_id.items(): region = regions[location_table[location_name].er_region] location = TunicERLocation(world.player, location_name, location_id, region) region.locations.append(location) - - create_randomized_entrances(portal_pairs, regions) for region in regions.values(): world.multiworld.regions.append(region) @@ -67,9 +82,9 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: "Eastern Vault West Fuses": "Eastern Vault Fortress", "Eastern Vault East Fuse": "Eastern Vault Fortress", "Quarry Connector Fuse": "Quarry Connector", - "Quarry Fuse": "Quarry", + "Quarry Fuse": "Quarry Entry", "Ziggurat Fuse": "Rooted Ziggurat Lower Back", - "West Garden Fuse": "West Garden", + "West Garden Fuse": "West Garden South Checkpoint", "Library Fuse": "Library Lab", "Place Questagons": "Sealed Temple", } @@ -93,10 +108,22 @@ 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"] + portal_map = [portal for portal in portal_mapping if portal.name not in + ["Ziggurat Lower Falling Entrance", "Purgatory Bottom Exit", "Purgatory Top Exit"]] while portal_map: portal1 = portal_map[0] @@ -105,11 +132,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="_") - - elif portal2_sdt == "Purgatory, Purgatory_bottom": - portal2_sdt = "Purgatory, Purgatory_top" + create_shop_region(world, regions) for portal in portal_map: if portal.scene_destination() == portal2_sdt: @@ -125,14 +150,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.multiworld.get_player_name(world.player) + 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 +168,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 +197,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 +224,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 +253,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 +269,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 +318,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 +357,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 +372,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,8 +424,9 @@ 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 + # if not waterfall_plando, then we just want to pair secret gathering place now elif portal.region != "Secret Gathering Place": continue portal2 = portal @@ -405,9 +437,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 +450,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 +488,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 +512,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 - elif req == "UR": - if logic < 2: + # 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.startswith("LS"): + if int(req[-1]) > ls: break elif req not in connected_regions: break @@ -504,3 +535,29 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic) return connected_regions + + +# sort the portal dict by the name of the first portal, referring to the portal order in the master portal list +def sort_portals(portal_pairs: Dict[Portal, Portal]) -> Dict[str, str]: + sorted_pairs: Dict[str, str] = {} + reference_list: List[str] = [portal.name for portal in portal_mapping] + reference_list.append("Shop Portal") + + # note: this is not necessary yet since the shop portals aren't numbered yet -- they will be when decoupled happens + # due to plando, there can be a variable number of shops + # I could either do it like this, or just go up to like 200, this seemed better + # shop_count = 0 + # for portal1, portal2 in portal_pairs.items(): + # if portal1.name.startswith("Shop"): + # shop_count += 1 + # if portal2.name.startswith("Shop"): + # shop_count += 1 + # reference_list.extend([f"Shop Portal {i + 1}" for i in range(shop_count)]) + + for name in reference_list: + for portal1, portal2 in portal_pairs.items(): + if name == portal1.name: + sorted_pairs[portal1.name] = portal2.name + break + return sorted_pairs + diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index a8aec9f74485..f30c1d5d248a 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -1,171 +1,174 @@ from itertools import groupby -from typing import Dict, List, Set, NamedTuple -from BaseClasses import ItemClassification +from typing import Dict, List, Set, NamedTuple, Optional +from BaseClasses import ItemClassification as IC class TunicItemData(NamedTuple): - classification: ItemClassification + classification: IC quantity_in_item_pool: int item_id_offset: int item_group: str = "" + # classification if combat logic is on + combat_ic: Optional[IC] = None item_base_id = 509342400 item_table: Dict[str, TunicItemData] = { - "Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "Bombs"), - "Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "Bombs"), - "Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "Bombs"), - "Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "Bombs"), - "Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "Bombs"), - "Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "Bombs"), - "Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "Bombs"), - "Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "Bombs"), - "Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "Bombs"), - "Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "Bombs"), - "Lure": TunicItemData(ItemClassification.filler, 4, 10, "Consumables"), - "Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "Consumables"), - "Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "Consumables"), - "Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "Consumables"), - "Effigy": TunicItemData(ItemClassification.useful, 12, 14, "Money"), - "HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "Consumables"), - "HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "Consumables"), - "HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "Consumables"), - "MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "Consumables"), - "MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "Consumables"), - "MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "Consumables"), - "Fairy": TunicItemData(ItemClassification.progression, 20, 21), - "Stick": TunicItemData(ItemClassification.progression, 1, 22, "Weapons"), - "Sword": TunicItemData(ItemClassification.progression, 3, 23, "Weapons"), - "Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "Weapons"), - "Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "Weapons"), - "Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26), - "Magic Orb": TunicItemData(ItemClassification.progression, 1, 27), - "Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28), - "Lantern": TunicItemData(ItemClassification.progression, 1, 29), - "Gun": TunicItemData(ItemClassification.useful, 1, 30, "Weapons"), - "Shield": TunicItemData(ItemClassification.useful, 1, 31), - "Dath Stone": TunicItemData(ItemClassification.useful, 1, 32), - "Hourglass": TunicItemData(ItemClassification.useful, 1, 33), - "Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "Keys"), - "Key": TunicItemData(ItemClassification.progression, 2, 35, "Keys"), - "Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "Keys"), - "Flask Shard": TunicItemData(ItemClassification.useful, 12, 37), - "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "Flask"), - "Golden Coin": TunicItemData(ItemClassification.progression, 17, 39), - "Card Slot": TunicItemData(ItemClassification.useful, 4, 40), - "Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "Hexagons"), - "Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "Hexagons"), - "Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "Hexagons"), - "Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "Hexagons"), - "ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "Offerings"), - "DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "Offerings"), - "Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "Offerings"), - "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "Offerings"), - "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "Offerings"), - "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "Offerings"), - "Hero Relic - ATT": TunicItemData(ItemClassification.progression_skip_balancing, 1, 51, "Hero Relics"), - "Hero Relic - DEF": TunicItemData(ItemClassification.progression_skip_balancing, 1, 52, "Hero Relics"), - "Hero Relic - HP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 53, "Hero Relics"), - "Hero Relic - MP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 54, "Hero Relics"), - "Hero Relic - POTION": TunicItemData(ItemClassification.progression_skip_balancing, 1, 55, "Hero Relics"), - "Hero Relic - SP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 56, "Hero Relics"), - "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "Cards"), - "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "Cards"), - "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "Cards"), - "Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "Cards"), - "Bracer": TunicItemData(ItemClassification.useful, 1, 61, "Cards"), - "Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "Cards"), - "Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "Cards"), - "Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "Cards"), - "Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "Cards"), - "Anklet": TunicItemData(ItemClassification.useful, 1, 66, "Cards"), - "Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "Cards"), - "Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "Cards"), - "Perfume": TunicItemData(ItemClassification.useful, 1, 69, "Cards"), - "Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "Cards"), - "Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "Cards"), - "Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "Cards"), - "Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "Golden Treasures"), - "Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "Golden Treasures"), - "Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "Golden Treasures"), - "Vintage": TunicItemData(ItemClassification.useful, 1, 76, "Golden Treasures"), - "Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "Golden Treasures"), - "Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "Golden Treasures"), - "Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "Golden Treasures"), - "Power Up": TunicItemData(ItemClassification.useful, 1, 80, "Golden Treasures"), - "Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "Golden Treasures"), - "Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "Golden Treasures"), - "Dusty": TunicItemData(ItemClassification.useful, 1, 83, "Golden Treasures"), - "Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "Golden Treasures"), - "Fool Trap": TunicItemData(ItemClassification.trap, 0, 85), - "Money x1": TunicItemData(ItemClassification.filler, 3, 86, "Money"), - "Money x10": TunicItemData(ItemClassification.filler, 1, 87, "Money"), - "Money x15": TunicItemData(ItemClassification.filler, 10, 88, "Money"), - "Money x16": TunicItemData(ItemClassification.filler, 1, 89, "Money"), - "Money x20": TunicItemData(ItemClassification.filler, 17, 90, "Money"), - "Money x25": TunicItemData(ItemClassification.filler, 14, 91, "Money"), - "Money x30": TunicItemData(ItemClassification.filler, 4, 92, "Money"), - "Money x32": TunicItemData(ItemClassification.filler, 4, 93, "Money"), - "Money x40": TunicItemData(ItemClassification.filler, 3, 94, "Money"), - "Money x48": TunicItemData(ItemClassification.filler, 1, 95, "Money"), - "Money x50": TunicItemData(ItemClassification.filler, 7, 96, "Money"), - "Money x64": TunicItemData(ItemClassification.filler, 1, 97, "Money"), - "Money x100": TunicItemData(ItemClassification.filler, 5, 98, "Money"), - "Money x128": TunicItemData(ItemClassification.useful, 3, 99, "Money"), - "Money x200": TunicItemData(ItemClassification.useful, 1, 100, "Money"), - "Money x255": TunicItemData(ItemClassification.useful, 1, 101, "Money"), - "Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "Pages"), - "Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "Pages"), - "Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "Pages"), - "Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "Pages"), - "Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "Pages"), - "Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "Pages"), - "Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "Pages"), - "Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "Pages"), - "Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "Pages"), - "Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "Pages"), - "Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "Pages"), - "Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "Pages"), - "Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "Pages"), - "Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "Pages"), - "Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "Pages"), - "Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "Pages"), - "Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "Pages"), - "Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "Pages"), - "Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "Pages"), - "Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "Pages"), - "Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "Pages"), - "Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "Pages"), - "Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "Pages"), - "Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "Pages"), - "Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "Pages"), - "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "Pages"), - "Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "Pages"), - "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "Pages"), - "Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "Ladders"), - "Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "Ladders"), - "Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "Ladders"), - "Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "Ladders"), - "Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "Ladders"), - "Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "Ladders"), - "Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "Ladders"), - "Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "Ladders"), - "Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "Ladders"), - "Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "Ladders"), - "Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "Ladders"), - "Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "Ladders"), - "Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "Ladders"), - "Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "Ladders"), - "Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "Ladders"), - "Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "Ladders"), - "Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "Ladders"), - "Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "Ladders"), - "Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "Ladders"), - "Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "Ladders"), - "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"), + "Firecracker x2": TunicItemData(IC.filler, 3, 0, "Bombs"), + "Firecracker x3": TunicItemData(IC.filler, 3, 1, "Bombs"), + "Firecracker x4": TunicItemData(IC.filler, 3, 2, "Bombs"), + "Firecracker x5": TunicItemData(IC.filler, 1, 3, "Bombs"), + "Firecracker x6": TunicItemData(IC.filler, 2, 4, "Bombs"), + "Fire Bomb x2": TunicItemData(IC.filler, 2, 5, "Bombs"), + "Fire Bomb x3": TunicItemData(IC.filler, 1, 6, "Bombs"), + "Ice Bomb x2": TunicItemData(IC.filler, 2, 7, "Bombs"), + "Ice Bomb x3": TunicItemData(IC.filler, 2, 8, "Bombs"), + "Ice Bomb x5": TunicItemData(IC.filler, 1, 9, "Bombs"), + "Lure": TunicItemData(IC.filler, 4, 10, "Consumables"), + "Lure x2": TunicItemData(IC.filler, 1, 11, "Consumables"), + "Pepper x2": TunicItemData(IC.filler, 4, 12, "Consumables"), + "Ivy x3": TunicItemData(IC.filler, 2, 13, "Consumables"), + "Effigy": TunicItemData(IC.useful, 12, 14, "Money", combat_ic=IC.progression), + "HP Berry": TunicItemData(IC.filler, 2, 15, "Consumables"), + "HP Berry x2": TunicItemData(IC.filler, 4, 16, "Consumables"), + "HP Berry x3": TunicItemData(IC.filler, 2, 17, "Consumables"), + "MP Berry": TunicItemData(IC.filler, 4, 18, "Consumables"), + "MP Berry x2": TunicItemData(IC.filler, 2, 19, "Consumables"), + "MP Berry x3": TunicItemData(IC.filler, 7, 20, "Consumables"), + "Fairy": TunicItemData(IC.progression, 20, 21), + "Stick": TunicItemData(IC.progression | IC.useful, 1, 22, "Weapons"), + "Sword": TunicItemData(IC.progression | IC.useful, 3, 23, "Weapons"), + "Sword Upgrade": TunicItemData(IC.progression | IC.useful, 4, 24, "Weapons"), + "Magic Wand": TunicItemData(IC.progression | IC.useful, 1, 25, "Weapons"), + "Magic Dagger": TunicItemData(IC.progression | IC.useful, 1, 26), + "Magic Orb": TunicItemData(IC.progression | IC.useful, 1, 27), + "Hero's Laurels": TunicItemData(IC.progression | IC.useful, 1, 28), + "Lantern": TunicItemData(IC.progression, 1, 29), + "Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"), + "Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful), + "Dath Stone": TunicItemData(IC.useful, 1, 32), + "Hourglass": TunicItemData(IC.useful, 1, 33), + "Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"), + "Key": TunicItemData(IC.progression, 2, 35, "Keys"), + "Fortress Vault Key": TunicItemData(IC.progression, 1, 36, "Keys"), + "Flask Shard": TunicItemData(IC.useful, 12, 37, combat_ic=IC.progression), + "Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask", combat_ic=IC.progression), + "Golden Coin": TunicItemData(IC.progression, 17, 39), + "Card Slot": TunicItemData(IC.useful, 4, 40), + "Red Questagon": TunicItemData(IC.progression_skip_balancing, 1, 41, "Hexagons"), + "Green Questagon": TunicItemData(IC.progression_skip_balancing, 1, 42, "Hexagons"), + "Blue Questagon": TunicItemData(IC.progression_skip_balancing, 1, 43, "Hexagons"), + "Gold Questagon": TunicItemData(IC.progression_skip_balancing, 0, 44, "Hexagons"), + "ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings", combat_ic=IC.progression), + "DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings", combat_ic=IC.progression), + "Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings", combat_ic=IC.progression), + "HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings", combat_ic=IC.progression), + "MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings", combat_ic=IC.progression), + "SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings", combat_ic=IC.progression), + "Hero Relic - ATT": TunicItemData(IC.progression_skip_balancing, 1, 51, "Hero Relics", combat_ic=IC.progression), + "Hero Relic - DEF": TunicItemData(IC.progression_skip_balancing, 1, 52, "Hero Relics", combat_ic=IC.progression), + "Hero Relic - HP": TunicItemData(IC.progression_skip_balancing, 1, 53, "Hero Relics", combat_ic=IC.progression), + "Hero Relic - MP": TunicItemData(IC.progression_skip_balancing, 1, 54, "Hero Relics", combat_ic=IC.progression), + "Hero Relic - POTION": TunicItemData(IC.progression_skip_balancing, 1, 55, "Hero Relics", combat_ic=IC.progression), + "Hero Relic - SP": TunicItemData(IC.progression_skip_balancing, 1, 56, "Hero Relics", combat_ic=IC.progression), + "Orange Peril Ring": TunicItemData(IC.useful, 1, 57, "Cards"), + "Tincture": TunicItemData(IC.useful, 1, 58, "Cards"), + "Scavenger Mask": TunicItemData(IC.progression, 1, 59, "Cards"), + "Cyan Peril Ring": TunicItemData(IC.useful, 1, 60, "Cards"), + "Bracer": TunicItemData(IC.useful, 1, 61, "Cards"), + "Dagger Strap": TunicItemData(IC.useful, 1, 62, "Cards"), + "Inverted Ash": TunicItemData(IC.useful, 1, 63, "Cards"), + "Lucky Cup": TunicItemData(IC.useful, 1, 64, "Cards"), + "Magic Echo": TunicItemData(IC.useful, 1, 65, "Cards"), + "Anklet": TunicItemData(IC.useful, 1, 66, "Cards"), + "Muffling Bell": TunicItemData(IC.useful, 1, 67, "Cards"), + "Glass Cannon": TunicItemData(IC.useful, 1, 68, "Cards"), + "Perfume": TunicItemData(IC.useful, 1, 69, "Cards"), + "Louder Echo": TunicItemData(IC.useful, 1, 70, "Cards"), + "Aura's Gem": TunicItemData(IC.useful, 1, 71, "Cards"), + "Bone Card": TunicItemData(IC.useful, 1, 72, "Cards"), + "Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures", combat_ic=IC.progression), + "Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures", combat_ic=IC.progression), + "Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures", combat_ic=IC.progression), + "Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures", combat_ic=IC.progression), + "Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures", combat_ic=IC.progression), + "Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures", combat_ic=IC.progression), + "Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures", combat_ic=IC.progression), + "Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures", combat_ic=IC.progression), + "Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures", combat_ic=IC.progression), + "Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures", combat_ic=IC.progression), + "Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures", combat_ic=IC.progression), + "Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures", combat_ic=IC.progression), + "Fool Trap": TunicItemData(IC.trap, 0, 85), + "Money x1": TunicItemData(IC.filler, 3, 86, "Money"), + "Money x10": TunicItemData(IC.filler, 1, 87, "Money"), + "Money x15": TunicItemData(IC.filler, 10, 88, "Money"), + "Money x16": TunicItemData(IC.filler, 1, 89, "Money"), + "Money x20": TunicItemData(IC.filler, 17, 90, "Money"), + "Money x25": TunicItemData(IC.filler, 14, 91, "Money"), + "Money x30": TunicItemData(IC.filler, 4, 92, "Money"), + "Money x32": TunicItemData(IC.filler, 4, 93, "Money"), + "Money x40": TunicItemData(IC.filler, 3, 94, "Money"), + "Money x48": TunicItemData(IC.filler, 1, 95, "Money"), + "Money x50": TunicItemData(IC.filler, 7, 96, "Money"), + "Money x64": TunicItemData(IC.filler, 1, 97, "Money"), + "Money x100": TunicItemData(IC.filler, 5, 98, "Money"), + "Money x128": TunicItemData(IC.useful, 3, 99, "Money", combat_ic=IC.progression), + "Money x200": TunicItemData(IC.useful, 1, 100, "Money", combat_ic=IC.progression), + "Money x255": TunicItemData(IC.useful, 1, 101, "Money", combat_ic=IC.progression), + "Pages 0-1": TunicItemData(IC.useful, 1, 102, "Pages"), + "Pages 2-3": TunicItemData(IC.useful, 1, 103, "Pages"), + "Pages 4-5": TunicItemData(IC.useful, 1, 104, "Pages"), + "Pages 6-7": TunicItemData(IC.useful, 1, 105, "Pages"), + "Pages 8-9": TunicItemData(IC.useful, 1, 106, "Pages"), + "Pages 10-11": TunicItemData(IC.useful, 1, 107, "Pages"), + "Pages 12-13": TunicItemData(IC.useful, 1, 108, "Pages"), + "Pages 14-15": TunicItemData(IC.useful, 1, 109, "Pages"), + "Pages 16-17": TunicItemData(IC.useful, 1, 110, "Pages"), + "Pages 18-19": TunicItemData(IC.useful, 1, 111, "Pages"), + "Pages 20-21": TunicItemData(IC.useful, 1, 112, "Pages"), + "Pages 22-23": TunicItemData(IC.useful, 1, 113, "Pages"), + "Pages 24-25 (Prayer)": TunicItemData(IC.progression | IC.useful, 1, 114, "Pages"), + "Pages 26-27": TunicItemData(IC.useful, 1, 115, "Pages"), + "Pages 28-29": TunicItemData(IC.useful, 1, 116, "Pages"), + "Pages 30-31": TunicItemData(IC.useful, 1, 117, "Pages"), + "Pages 32-33": TunicItemData(IC.useful, 1, 118, "Pages"), + "Pages 34-35": TunicItemData(IC.useful, 1, 119, "Pages"), + "Pages 36-37": TunicItemData(IC.useful, 1, 120, "Pages"), + "Pages 38-39": TunicItemData(IC.useful, 1, 121, "Pages"), + "Pages 40-41": TunicItemData(IC.useful, 1, 122, "Pages"), + "Pages 42-43 (Holy Cross)": TunicItemData(IC.progression | IC.useful, 1, 123, "Pages"), + "Pages 44-45": TunicItemData(IC.useful, 1, 124, "Pages"), + "Pages 46-47": TunicItemData(IC.useful, 1, 125, "Pages"), + "Pages 48-49": TunicItemData(IC.useful, 1, 126, "Pages"), + "Pages 50-51": TunicItemData(IC.useful, 1, 127, "Pages"), + "Pages 52-53 (Icebolt)": TunicItemData(IC.progression, 1, 128, "Pages"), + "Pages 54-55": TunicItemData(IC.useful, 1, 129, "Pages"), + "Ladders near Weathervane": TunicItemData(IC.progression, 0, 130, "Ladders"), + "Ladders near Overworld Checkpoint": TunicItemData(IC.progression, 0, 131, "Ladders"), + "Ladders near Patrol Cave": TunicItemData(IC.progression, 0, 132, "Ladders"), + "Ladder near Temple Rafters": TunicItemData(IC.progression, 0, 133, "Ladders"), + "Ladders near Dark Tomb": TunicItemData(IC.progression, 0, 134, "Ladders"), + "Ladder to Quarry": TunicItemData(IC.progression, 0, 135, "Ladders"), + "Ladders to West Bell": TunicItemData(IC.progression, 0, 136, "Ladders"), + "Ladders in Overworld Town": TunicItemData(IC.progression, 0, 137, "Ladders"), + "Ladder to Ruined Atoll": TunicItemData(IC.progression, 0, 138, "Ladders"), + "Ladder to Swamp": TunicItemData(IC.progression, 0, 139, "Ladders"), + "Ladders in Well": TunicItemData(IC.progression, 0, 140, "Ladders"), + "Ladder in Dark Tomb": TunicItemData(IC.progression, 0, 141, "Ladders"), + "Ladder to East Forest": TunicItemData(IC.progression, 0, 142, "Ladders"), + "Ladders to Lower Forest": TunicItemData(IC.progression, 0, 143, "Ladders"), + "Ladder to Beneath the Vault": TunicItemData(IC.progression, 0, 144, "Ladders"), + "Ladders in Hourglass Cave": TunicItemData(IC.progression, 0, 145, "Ladders"), + "Ladders in South Atoll": TunicItemData(IC.progression, 0, 146, "Ladders"), + "Ladders to Frog's Domain": TunicItemData(IC.progression, 0, 147, "Ladders"), + "Ladders in Library": TunicItemData(IC.progression, 0, 148, "Ladders"), + "Ladders in Lower Quarry": TunicItemData(IC.progression, 0, 149, "Ladders"), + "Ladders in Swamp": TunicItemData(IC.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 +176,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", @@ -204,9 +208,13 @@ class TunicItemData(NamedTuple): "Gold Questagon", ] +combat_items: List[str] = [name for name, data in item_table.items() + if data.combat_ic and IC.progression in data.combat_ic] +combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels"]) + item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} -filler_items: List[str] = [name for name, data in item_table.items() if data.classification == ItemClassification.filler] +filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler] def get_item_group(item_name: str) -> str: @@ -235,9 +243,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..f2d4b94406ac --- /dev/null +++ b/worlds/tunic/ladder_storage_data.py @@ -0,0 +1,189 @@ +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", "Furnace_gyro_west", "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 before Boss", "Archipelagos Redux, Overworld Redux_upper"), + # West Garden laurels exit + LadderInfo("West Garden after Terry", "Archipelagos Redux, Overworld Redux_lowest"), + # Magic dagger house, only relevant with combat logic on + LadderInfo("West Garden after Terry", "Archipelagos Redux, archipelagos_house_"), + + # 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 Entry", dest_is_region=True), + LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Mid Checkpoint", 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 Front", "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 Front", "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 2d87140fe50f..5ea309fb19d7 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -25,17 +25,17 @@ class TunicLocationData(NamedTuple): "Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"), "Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"), "Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Overworld", "Dark Tomb Checkpoint"), - "Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"), - "Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"), + "Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"), + "Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral Entry"), # entry because special rules + "Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral Main"), + "Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral Main"), + "Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral Entry"), + "Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral Main"), + "Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"), + "Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral Main"), + "Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral Entry"), "Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"), - "Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Dark Exit"), "Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), "Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"), "Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Upper"), @@ -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"), @@ -81,25 +81,25 @@ class TunicLocationData(NamedTuple): "Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), "Eastern Vault Fortress - [West Wing] Page Pickup": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), "Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"), - "Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), - "Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), + "Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path by Grave"), + "Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path by Grave"), "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"), "Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Main"), "Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), - "Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain Main"), + "Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain Main"), "Frog's Domain - Escape Chest": TunicLocationData("Frog's Domain", "Frog's Domain Back"), - "Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"), + "Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain Front"), + "Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain Front"), + "Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain Main"), + "Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain Main"), + "Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain Main"), + "Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain Main"), + "Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain Front"), + "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain Main"), "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"), "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"), "Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"), @@ -131,7 +131,7 @@ class TunicLocationData(NamedTuple): "Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld Beach"), "Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld Tunnel to Beach"), "Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld after Envoy"), "Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld Swamp Lower Entry"), @@ -158,7 +158,7 @@ class TunicLocationData(NamedTuple): "Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Upper Overworld"), "Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld Well Entry Area"), "Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"), "Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"), "Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"), @@ -205,18 +205,18 @@ 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", "Quarry"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), - "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), @@ -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"), @@ -233,17 +233,17 @@ class TunicLocationData(NamedTuple): "Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Even Lower Quarry"), "Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"), - "Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry"), + "Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry Isolated Chest"), "Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), "Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"), "Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"), "Rooted Ziggurat Tower - Inside Tower": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Middle Top"), - "Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), - "Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), - "Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), - "Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"), + "Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"), + "Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"), + "Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"), "Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), - "Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), + "Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"), "Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="Bosses"), "Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"), @@ -290,26 +290,26 @@ class TunicLocationData(NamedTuple): "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"), "West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"), "Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"), - "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"), - "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"), - "West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"), - "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"), - "West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"), - "West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden"), - "West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden"), - "West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"), - "West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden"), - "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"), + "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden before Boss", location_group="Holy Cross"), + "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden after Terry", location_group="Holy Cross"), + "West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden at Dagger House"), + "West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden South Checkpoint"), + "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden before Terry", location_group="Holy Cross"), + "West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden before Boss"), + "West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden after Terry"), + "West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden before Terry"), + "West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden after Terry"), + "West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden after Terry"), + "West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden after Terry"), + "West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden before Terry"), + "West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden after Terry"), + "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden South Checkpoint"), + "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden South Checkpoint"), + "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden before Boss"), "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="Bosses"), - "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"), + "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden South Checkpoint"), "West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"), - "West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"), + "West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden before Terry"), "West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"), "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"), } diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index bf1dd860ae79..24247a6cfdcf 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. @@ -132,8 +111,10 @@ class EntranceRando(TextChoice): internal_name = "entrance_rando" display_name = "Entrance Rando" alias_false = 0 + alias_off = 0 option_no = 0 alias_true = 1 + alias_on = 1 option_yes = 1 default = 0 @@ -171,8 +152,8 @@ class ShuffleLadders(Toggle): """ internal_name = "shuffle_ladders" display_name = "Shuffle Ladders" - - + + class TunicPlandoConnections(PlandoConnections): """ Generic connection plando. Format is: @@ -187,6 +168,98 @@ class TunicPlandoConnections(PlandoConnections): duplicate_exits = True +class CombatLogic(Choice): + """ + If enabled, the player will logically require a combination of stat upgrade items and equipment to get some checks or navigate to some areas, with a goal of matching the vanilla combat difficulty. + The player may still be expected to run past enemies, reset aggro (by using a checkpoint or doing a scene transition), or find sneaky paths to checks. + This option marks many more items as progression and may force weapons much earlier than normal. + Bosses Only makes it so that additional combat logic is only added to the boss fights and the Gauntlet. + If disabled, the standard, looser logic is used. The standard logic does not include stat upgrades, just minimal weapon requirements, such as requiring a Sword or Magic Wand for Quarry, or not requiring a weapon for Swamp. + """ + internal_name = "combat_logic" + display_name = "More Combat Logic" + option_off = 0 + option_bosses_only = 1 + option_on = 2 + default = 0 + + +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. + Enabling any of these difficulty 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 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, Magic Orb, or Shield 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 @@ -197,22 +270,32 @@ 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 + combat_logic: CombatLogic 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, + CombatLogic, Lanternless, Maskless, + LaurelsZips, + IceGrappling, + LadderStorage, + LadderStorageWithoutItems ]) ] @@ -229,9 +312,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 97270b5a2a81..30b7cee9d07b 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -1,9 +1,9 @@ from random import Random from typing import Dict, TYPE_CHECKING -from worlds.generic.Rules import set_rule, forbid_item +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 @@ -11,12 +11,14 @@ grapple = "Magic Orb" ice_dagger = "Magic Dagger" fire_wand = "Magic Wand" +gun = "Gun" lantern = "Lantern" fairies = "Fairy" coins = "Golden Coin" prayer = "Pages 24-25 (Prayer)" holy_cross = "Pages 42-43 (Holy Cross)" icebolt = "Pages 52-53 (Icebolt)" +shield = "Shield" key = "Key" house_key = "Old House Key" vault_key = "Fortress Vault Key" @@ -26,6 +28,11 @@ 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", "Ruined Atoll - [Northwest] Bombable Wall"] + def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: ability_requirement = [1, 1, 1] @@ -38,302 +45,345 @@ def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str return dict(zip(abilities, ability_requirement)) -def has_ability(state: CollectionState, player: int, ability: str, options: TunicOptions, - ability_unlocks: Dict[str, int]) -> bool: +def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool: + options = world.options + ability_unlocks = world.ability_unlocks if not options.ability_shuffling: return True if options.hexagon_quest: - return state.has(gold_hexagon, player, ability_unlocks[ability]) - return state.has(ability, player) + return state.has(gold_hexagon, world.player, ability_unlocks[ability]) + return state.has(ability, world.player) # a check to see if you can whack things in melee at all -def has_stick(state: CollectionState, player: int) -> bool: - return state.has("Stick", player) or state.has("Sword Upgrade", player, 1) or state.has("Sword", player) +def has_melee(state: CollectionState, player: int) -> bool: + return state.has_any({"Stick", "Sword", "Sword Upgrade"}, player) 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, player: int, options: TunicOptions, - ability_unlocks: Dict[str, int]) -> bool: - if not options.logic_rules: - return False +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(state, player, icebolt, options, ability_unlocks) + return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world) -def can_ladder_storage(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.logic_rules == "unrestricted" and has_stick(state, player): - return True - else: +def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: + if not world.options.ladder_storage: return False + if world.options.ladder_storage_without_items: + return True + return has_melee(state, world.player) or state.has_any((grapple, shield), world.player) -def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.maskless: - return True - else: - return state.has(mask, player) +def has_mask(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.maskless or state.has(mask, world.player) -def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.lanternless: - return True - else: - return state.has(lantern, player) +def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.lanternless or state.has(lantern, world.player) -def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: - multiworld = world.multiworld +def set_region_rules(world: "TunicWorld") -> None: player = world.player options = world.options - multiworld.get_entrance("Overworld -> Overworld Holy Cross", player).access_rule = \ - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) - multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \ - lambda state: has_stick(state, player) or state.has(fire_wand, player) - multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \ - lambda state: has_lantern(state, player, options) - multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ + world.get_entrance("Overworld -> Overworld Holy Cross").access_rule = \ + lambda state: has_ability(holy_cross, state, world) + world.get_entrance("Overworld -> Beneath the Well").access_rule = \ + lambda state: has_melee(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) + 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 can_ladder_storage(state, player, options) - multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ - lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ - or can_ladder_storage(state, player, options) + 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 - multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \ - lambda state: has_lantern(state, player, options) and \ - has_ability(state, player, prayer, options, ability_unlocks) - multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \ - lambda state: state.has_any({grapple, laurels}, player) and \ - has_ability(state, player, prayer, options, ability_unlocks) - multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \ + world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ + lambda state: (has_lantern(state, world) and has_ability(prayer, state, world) + # there's some boxes in the way + and (has_melee(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 = \ lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \ - and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, player, options)) - multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \ + and (state.has_any({grapple, laurels, gun}, player) or can_ladder_storage(state, world)) + world.get_entrance("Quarry Back -> Quarry").access_rule = \ lambda state: has_sword(state, player) or state.has(fire_wand, player) - multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \ - lambda state: has_mask(state, player, options) - multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ - lambda state: state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks) - multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks) \ - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) - multiworld.get_entrance("Overworld -> Spirit Arena", player).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) and state.has_group_unique("Hero Relics", player, 6)) and \ - has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player) and \ - state.has_any({lantern, laurels}, player) - - -def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: - multiworld = world.multiworld + world.get_entrance("Quarry -> Lower Quarry").access_rule = \ + lambda state: has_mask(state, world) + 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, 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) + and state.has_group_unique("Hero Relics", player, 6)) + 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(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) # Ability Shuffle Exclusive Rules - set_rule(multiworld.get_location("Far Shore - Page Pickup", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) - or can_ladder_storage(state, player, options) - or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and has_lantern(state, player, options))) - set_rule(multiworld.get_location("Fortress Courtyard - Page Near Cave", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) - or can_ladder_storage(state, player, options) - or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and has_lantern(state, player, options))) - set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(world.get_location("Far Shore - Page Pickup"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Fortress Courtyard - Chest 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, 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, 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"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Library Hall - Holy Cross Chest"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"), + lambda state: has_ability(holy_cross, state, world)) + set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"), + lambda state: has_ability(holy_cross, state, world)) # Overworld - set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + set_rule(world.get_location("Overworld - [Southwest] Fountain Page"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Far Shore - Secret Chest", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + set_rule(world.get_location("Far Shore - Secret Chest"), + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Old House - Normal Chest", player), + set_rule(world.get_location("Old House - Normal Chest"), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) - or (state.has(laurels, player) and options.logic_rules)) - set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and - (state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) - or (state.has(laurels, player) and options.logic_rules))) - set_rule(multiworld.get_location("Old House - Shield Pickup", player), + 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, 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, player, options, ability_unlocks) - or (state.has(laurels, player) and options.logic_rules)) - set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + 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(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + set_rule(world.get_location("Overworld - [Southwest] From West Garden"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), + set_rule(world.get_location("Overworld - [West] Chest After Bell"), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) - set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player), - lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) - set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + 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)) + set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), + set_rule(world.get_location("Special Shop - Secret Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and - (state.has(laurels, player) - or (has_lantern(state, player, options) and - (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) - set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player), + set_rule(world.get_location("Sealed Temple - Holy Cross Chest"), + 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, IceGrappling.option_medium, state, world))) + set_rule(world.get_location("Sealed Temple - Page Pickup"), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) - set_rule(multiworld.get_location("West Furnace - Lantern Pickup", player), - lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) + or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + set_rule(world.get_location("West Furnace - Lantern Pickup"), + lambda state: has_melee(state, player) or state.has_any({fire_wand, laurels}, player)) - set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"), lambda state: state.has(fairies, player, 10)) - set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + set_rule(world.get_location("Coins in the Well - 3 Coins"), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + set_rule(world.get_location("Coins in the Well - 6 Coins"), lambda state: state.has(coins, player, 6)) - set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + set_rule(world.get_location("Coins in the Well - 10 Coins"), lambda state: state.has(coins, player, 10)) - set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + set_rule(world.get_location("Coins in the Well - 15 Coins"), lambda state: state.has(coins, player, 15)) # East Forest - set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + set_rule(world.get_location("East Forest - Lower Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + set_rule(world.get_location("East Forest - Lower Dash Chest"), lambda state: state.has_all({grapple, laurels}, player)) - set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), + set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player) - and has_ability(state, player, icebolt, options, ability_unlocks)) + and has_ability(icebolt, state, world)) # West Garden - set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), + set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + set_rule(world.get_location("West Garden - [West] In Flooded Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), - lambda state: state.has(laurels, player) - and has_ability(state, player, holy_cross, options, ability_unlocks)) - set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), - lambda state: (state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) - set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), + 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, 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(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), + set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or (has_lantern(state, world) and has_sword(state, player)) + or can_ladder_storage(state, world)) # Ruined Atoll - set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Librarian - Hexagon Green", player), - lambda state: has_sword(state, player) or options.logic_rules) + # 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) + 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) + 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)) # Frog's Domain - set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + 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_melee(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 2"), + lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player)) + set_rule(world.get_location("Library Lab - Page 3"), + lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player)) + # Eastern Vault Fortress - set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player), - lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), - lambda state: has_sword(state, player) and - (has_ability(state, player, prayer, options, ability_unlocks) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) - set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), - lambda state: state.has(vault_key, player) and - (has_ability(state, player, prayer, options, ability_unlocks) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + # 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) and (has_melee(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, 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, IceGrappling.option_medium, state, world))) # Beneath the Vault - set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), - lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) - set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), - lambda state: has_stick(state, player) and has_lantern(state, player, options)) + set_rule(world.get_location("Beneath the Fortress - Bridge"), + lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player)) + set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"), + lambda state: has_melee(state, player) and has_lantern(state, world)) # Quarry - set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", 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(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), + lambda state: has_sword(state, player)) # Swamp - set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + 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, player, options, ability_unlocks))) - set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + 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(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player), + set_rule(world.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) # Hero's Grave - set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + set_rule(world.get_location("Hero's Grave - Tooth Relic"), + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + set_rule(world.get_location("Hero's Grave - Mushroom Relic"), + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + set_rule(world.get_location("Hero's Grave - Ash Relic"), + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + set_rule(world.get_location("Hero's Grave - Flowers Relic"), + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + set_rule(world.get_location("Hero's Grave - Effigy Relic"), + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + set_rule(world.get_location("Hero's Grave - Feathers Relic"), + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + + # 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) + 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 + set_rule(world.get_location("Shop - Potion 1"), + lambda state: has_sword(state, player)) + set_rule(world.get_location("Shop - Potion 2"), + lambda state: has_sword(state, player)) + set_rule(world.get_location("Shop - Coin 1"), + lambda state: has_sword(state, player)) + set_rule(world.get_location("Shop - Coin 2"), + lambda state: has_sword(state, player)) diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index 72d4a498d1ee..24551a13d547 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -3,6 +3,8 @@ class TestAccess(TunicTestBase): + options = {options.CombatLogic.internal_name: options.CombatLogic.option_off} + # test whether you can get into the temple without laurels def test_temple_access(self) -> None: self.collect_all_but(["Hero's Laurels", "Lantern"]) @@ -61,10 +63,66 @@ def test_normal_goal(self) -> None: class TestER(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.HexagonQuest.internal_name: options.HexagonQuest.option_false, + options.CombatLogic.internal_name: options.CombatLogic.option_off, + options.FixedShop.internal_name: options.FixedShop.option_true} def test_overworld_hc_chest(self) -> None: # test to see that static connections are working properly -- this chest requires holy cross and is in Overworld 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/undertale/__init__.py b/worlds/undertale/__init__.py index 9084c77b0065..9f09bb34526b 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -67,12 +67,15 @@ def _get_undertale_data(self): "only_flakes": bool(self.options.only_flakes.value), "no_equips": bool(self.options.no_equips.value), "key_hunt": bool(self.options.key_hunt.value), - "key_pieces": self.options.key_pieces.value, - "rando_love": bool(self.options.rando_love.value), - "rando_stats": bool(self.options.rando_stats.value), + "key_pieces": int(self.options.key_pieces.value), + "rando_love": bool(self.options.rando_love and (self.options.route_required == "genocide" or self.options.route_required == "all_routes")), + "rando_stats": bool(self.options.rando_stats and (self.options.route_required == "genocide" or self.options.route_required == "all_routes")), "prog_armor": bool(self.options.prog_armor.value), "prog_weapons": bool(self.options.prog_weapons.value), - "rando_item_button": bool(self.options.rando_item_button.value) + "rando_item_button": bool(self.options.rando_item_button.value), + "route_required": int(self.options.route_required.value), + "temy_include": int(self.options.temy_include.value) + } def get_filler_item_name(self): @@ -220,16 +223,7 @@ def UndertaleRegion(region_name: str, exits=[]): link_undertale_areas(self.multiworld, self.player) def fill_slot_data(self): - slot_data = self._get_undertale_data() - for option_name in self.options.as_dict(): - option = getattr(self.multiworld, option_name)[self.player] - if (option_name == "rando_love" or option_name == "rando_stats") and \ - self.options.route_required != "genocide" and \ - self.options.route_required != "all_routes": - option.value = False - if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: - slot_data[option_name] = int(option.value) - return slot_data + return self._get_undertale_data() def create_item(self, name: str) -> Item: item_data = item_table[name] diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 3d3ee8cf58fd..b74f335189cf 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -49,12 +49,14 @@ def set_rules(self): self.area_cost_map = {} set_rules(self.multiworld, self.options, self.player, self.area_connections, self.area_cost_map) - def create_item(self, name: str) -> Item: - return V6Item(name, ItemClassification.progression, item_table[name], self.player) + def create_item(self, name: str, classification: ItemClassification = ItemClassification.filler) -> Item: + return V6Item(name, classification, item_table[name], self.player) def create_items(self): - trinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range(0,20)] - self.multiworld.itempool += trinkets + progtrinkets = [self.create_item("Trinket " + str(i+1).zfill(2), ItemClassification.progression) for i in range(0, (4 * self.options.door_cost.value))] + filltrinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range((4 * self.options.door_cost.value), 20)] + self.multiworld.itempool += progtrinkets + self.multiworld.itempool += filltrinkets def generate_basic(self): musiclist_o = [1,2,3,4,9,12] diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index ecab25db3d71..ac9197bd92bb 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -11,11 +11,12 @@ from worlds.AutoWorld import WebWorld, World from .data import static_items as static_witness_items +from .data import static_locations as static_witness_locations from .data import static_logic as static_witness_logic from .data.item_definition_classes import DoorItemDefinition, ItemData -from .data.utils import get_audio_logs -from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints -from .locations import WitnessPlayerLocations, static_witness_locations +from .data.utils import cast_not_none, get_audio_logs +from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints +from .locations import WitnessPlayerLocations from .options import TheWitnessOptions, witness_option_groups from .player_items import WitnessItem, WitnessPlayerItems from .player_logic import WitnessPlayerLogic @@ -49,44 +50,51 @@ class WitnessWorld(World): topology_present = False web = WitnessWebWorld() + origin_region_name = "Entry" + options_dataclass = TheWitnessOptions options: TheWitnessOptions item_name_to_id = { - name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items() + # ITEM_DATA doesn't have any event items in it + name: cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() } location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID 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 player_items: WitnessPlayerItems player_regions: WitnessPlayerRegions - log_ids_to_hints: Dict[int, CompactItemData] - laser_ids_to_hints: Dict[int, CompactItemData] + log_ids_to_hints: Dict[int, CompactHintData] + laser_ids_to_hints: Dict[int, CompactHintData] items_placed_early: List[str] own_itempool: List[WitnessItem] + panel_hunt_required_count: int + def _get_slot_data(self) -> Dict[str, Any]: return { - "seed": self.random.randrange(0, 1000000), + "seed": self.options.puzzle_randomization_seed.value, "victory_location": int(self.player_logic.VICTORY_LOCATION, 16), "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID, "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(), "door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(), "symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(), "disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES], + "hunt_entities": [int(h, 16) for h in self.player_logic.HUNT_ENTITIES], "log_ids_to_hints": self.log_ids_to_hints, "laser_ids_to_hints": self.laser_ids_to_hints, "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(), "obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES, - "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS], + "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES], "entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME, + "panel_hunt_required_absolute": self.panel_hunt_required_count } def determine_sufficient_progression(self) -> None: @@ -122,10 +130,10 @@ def determine_sufficient_progression(self) -> None: ) if not has_locally_relevant_progression and self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression" + warning(f"{self.player_name}'s Witness world doesn't have any progression" f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.") elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough" + raise OptionError(f"{self.player_name}'s Witness world doesn't have enough" f" progression items that can be placed in other players' worlds. Please turn on Symbol" f" Shuffle, Door Shuffle, or Obelisk Keys.") @@ -142,13 +150,20 @@ def generate_early(self) -> None: ) self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self) - self.log_ids_to_hints = dict() + self.log_ids_to_hints = {} self.determine_sufficient_progression() if self.options.shuffle_lasers == "local": self.options.local_items.value |= self.item_name_groups["Lasers"] + if self.options.victory_condition == "panel_hunt": + total_panels = self.options.panel_hunt_total + required_percentage = self.options.panel_hunt_required_percentage + self.panel_hunt_required_count = round(total_panels * required_percentage / 100) + else: + self.panel_hunt_required_count = 0 + def create_regions(self) -> None: self.player_regions.create_regions(self, self.player_logic) @@ -167,7 +182,7 @@ def create_regions(self) -> None: for event_location in self.player_locations.EVENT_LOCATION_TABLE: item_obj = self.create_item( - self.player_logic.EVENT_ITEM_PAIRS[event_location] + self.player_logic.EVENT_ITEM_PAIRS[event_location][0] ) location_obj = self.get_location(event_location) location_obj.place_locked_item(item_obj) @@ -176,42 +191,49 @@ def create_regions(self) -> None: event_locations.append(location_obj) # Place other locked items - dog_puzzle_skip = self.create_item("Puzzle Skip") - self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) - - self.own_itempool.append(dog_puzzle_skip) - - self.items_placed_early.append("Puzzle Skip") - # Pick an early item to place on the tutorial gate. - early_items = [ - item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() - ] - if early_items: - random_early_item = self.random.choice(early_items) - if self.options.puzzle_randomization == "sigma_expert": - # In Expert, only tag the item as early, rather than forcing it onto the gate. - 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. - gate_item = self.create_item(random_early_item) - self.get_location("Tutorial Gate Open").place_locked_item(gate_item) - self.own_itempool.append(gate_item) - self.items_placed_early.append(random_early_item) + if self.options.shuffle_dog == "puzzle_skip": + dog_puzzle_skip = self.create_item("Puzzle Skip") + self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip) + + self.own_itempool.append(dog_puzzle_skip) + self.items_placed_early.append("Puzzle Skip") + + if self.options.early_symbol_item: + # Pick an early item to place on the tutorial gate. + early_items = [ + item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() + ] + if early_items: + random_early_item = self.random.choice(early_items) + 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. + gate_item = self.create_item(random_early_item) + self.get_location("Tutorial Gate Open").place_locked_item(gate_item) + self.own_itempool.append(gate_item) + self.items_placed_early.append(random_early_item) - # There are some really restrictive settings in The Witness. + # There are some really restrictive options in The Witness. # They are rarely played, but when they are, we add some extra sphere 1 locations. # This is done both to prevent generation failures, but also to make the early game less linear. # Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange. state = CollectionState(self.multiworld) - state.sweep_for_events(locations=event_locations) + state.sweep_for_advancements(locations=event_locations) - num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address) + num_early_locs = sum( + 1 for loc in self.multiworld.get_reachable_locations(state, self.player) + if loc.address and not loc.item + ) - # Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items + # Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items - needed_size = 3 + needed_size = 2 needed_size += self.options.puzzle_randomization == "sigma_expert" needed_size += self.options.shuffle_symbols needed_size += self.options.shuffle_doors > 0 @@ -233,9 +255,10 @@ def create_regions(self) -> None: self.player_locations.add_location_late(loc) self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}) - player = self.multiworld.get_player_name(self.player) - - warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""") + warning( + f"""Location "{loc}" had to be added to {self.player_name}'s world + due to insufficient sphere 1 size.""" + ) def create_items(self) -> None: # Determine pool size. @@ -272,17 +295,17 @@ def create_items(self) -> None: self.multiworld.push_precollected(self.create_item(inventory_item_name)) if len(item_pool) > pool_size: - error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})" + error(f"{self.player_name}'s Witness world has too few locations ({pool_size})" f" to place its necessary items ({len(item_pool)}).") return remaining_item_slots = pool_size - sum(item_pool.values()) # Add puzzle skips. - num_puzzle_skips = self.options.puzzle_skip_amount + num_puzzle_skips = self.options.puzzle_skip_amount.value if num_puzzle_skips > remaining_item_slots: - warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" + warning(f"{self.player_name}'s Witness world has insufficient locations" f" to place all requested puzzle skips.") num_puzzle_skips = remaining_item_slots item_pool["Puzzle Skip"] = num_puzzle_skips @@ -301,21 +324,21 @@ def create_items(self) -> None: if self.player_items.item_data[item_name].local_only: self.options.local_items.value.add(item_name) - def fill_slot_data(self) -> dict: - self.log_ids_to_hints: Dict[int, CompactItemData] = dict() - self.laser_ids_to_hints: Dict[int, CompactItemData] = dict() + def fill_slot_data(self) -> Dict[str, Any]: + self.log_ids_to_hints: Dict[int, CompactHintData] = {} + self.laser_ids_to_hints: Dict[int, CompactHintData] = {} already_hinted_locations = set() # Laser hints if self.options.laser_hints: - laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"]) + laser_hints = make_laser_hints(self, sorted(static_witness_items.ITEM_GROUPS["Lasers"])) for item_name, hint in laser_hints.items(): item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]) self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player) - already_hinted_locations.add(hint.location) + already_hinted_locations.add(cast_not_none(hint.location)) # Audio Log Hints @@ -378,13 +401,13 @@ class WitnessLocation(Location): game: str = "The Witness" entity_hex: int = -1 - def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None: + def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None: super().__init__(player, name, address, parent) self.entity_hex = ch_hex def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations, - region_locations=None, exits=None) -> Region: + region_locations: Optional[List[str]] = None, exits: Optional[List[str]] = None) -> Region: """ Create an Archipelago Region for The Witness """ @@ -399,11 +422,11 @@ def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlaye entity_hex = int( static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0 ) - location = WitnessLocation( + location_obj = WitnessLocation( world.player, location, loc_id, ret, entity_hex ) - ret.locations.append(location) + ret.locations.append(location_obj) if exits: for single_exit in exits: ret.exits.append(Entrance(world.player, single_exit, ret)) diff --git a/worlds/witness/data/WitnessItems.txt b/worlds/witness/data/WitnessItems.txt index 782fa9c3d226..57aee28e45b6 100644 --- a/worlds/witness/data/WitnessItems.txt +++ b/worlds/witness/data/WitnessItems.txt @@ -56,6 +56,7 @@ Doors: 1119 - Quarry Stoneworks Entry (Panel) - 0x01E5A,0x01E59 1120 - Quarry Stoneworks Ramp Controls (Panel) - 0x03678,0x03676 1122 - Quarry Stoneworks Lift Controls (Panel) - 0x03679,0x03675 +1123 - Quarry Stoneworks Stairs (Panel) - 0x03677 1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852 1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 1129 - Quarry Boathouse Hook Control (Panel) - 0x275FA @@ -84,6 +85,7 @@ Doors: 1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x17CBC 1208 - Treehouse Drawbridge (Panel) - 0x037FF 1175 - Jungle Popup Wall (Panel) - 0x17CAB +1178 - Jungle Monastery Garden Shortcut (Panel) - 0x17CAA 1180 - Bunker Entry (Panel) - 0x17C2E 1183 - Bunker Tinted Glass Door (Panel) - 0x0A099 1186 - Bunker Elevator Control (Panel) - 0x0A079 @@ -94,12 +96,15 @@ Doors: 1195 - Swamp Rotating Bridge (Panel) - 0x181F5 1196 - Swamp Long Bridge (Panel) - 0x17E2B 1197 - Swamp Maze Controls (Panel) - 0x17C0A,0x17E07 +1199 - Swamp Laser Shortcut (Panel) - 0x17C05 1220 - Mountain Floor 1 Light Bridge (Panel) - 0x09E39 1225 - Mountain Floor 2 Light Bridge Near (Panel) - 0x09E86 1230 - Mountain Floor 2 Light Bridge Far (Panel) - 0x09ED8 1235 - Mountain Floor 2 Elevator Control (Panel) - 0x09EEB 1240 - Caves Entry (Panel) - 0x00FF8 1242 - Caves Elevator Controls (Panel) - 0x335AB,0x335AC,0x3369D +1243 - Caves Mountain Shortcut (Panel) - 0x021D7 +1244 - Caves Swamp Shortcut (Panel) - 0x17CF2 1245 - Challenge Entry (Panel) - 0x0A16E 1250 - Tunnels Entry (Panel) - 0x039B4 1255 - Tunnels Town Shortcut (Panel) - 0x09E85 @@ -250,19 +255,20 @@ Doors: 2101 - Outside Tutorial Outpost Panels - 0x0A171,0x04CA4 2105 - Desert Panels - 0x09FAA,0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B,0x0C339,0x0A249,0x0A015,0x09FA0,0x09F86 2110 - Quarry Outside Panels - 0x17C09,0x09E57,0x17CC4 -2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675 +2115 - Quarry Stoneworks Panels - 0x01E5A,0x01E59,0x03678,0x03676,0x03679,0x03675,0x03677 2120 - Quarry Boathouse Panels - 0x03852,0x03858,0x275FA 2122 - Keep Hedge Maze Panels - 0x00139,0x019DC,0x019E7,0x01A0F 2125 - Monastery Panels - 0x09D9B,0x00C92,0x00B10 +2127 - Jungle Panels - 0x17CAB,0x17CAA 2130 - Town Church & RGB House Panels - 0x28998,0x28A0D,0x334D8 2135 - Town Maze Panels - 0x2896A,0x28A79 2137 - Town Dockside House Panels - 0x0A0C8,0x09F98 2140 - Windmill & Theater Panels - 0x17D02,0x00815,0x17F5F,0x17F89,0x0A168,0x33AB2 2145 - Treehouse Panels - 0x0A182,0x0288C,0x02886,0x2700B,0x17CBC,0x037FF 2150 - Bunker Panels - 0x34BC5,0x34BC6,0x0A079,0x0A099,0x17C2E -2155 - Swamp Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07,0x17C0D,0x0056E +2155 - Swamp Panels - 0x00609,0x18488,0x181F5,0x17E2B,0x17C0A,0x17E07,0x17C0D,0x0056E,0x17C05 2160 - Mountain Panels - 0x09ED8,0x09E86,0x09E39,0x09EEB -2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC +2165 - Caves Panels - 0x3369D,0x00FF8,0x0A16E,0x335AB,0x335AC,0x021D7,0x17CF2 2170 - Tunnels Panels - 0x09E85,0x039B4 2200 - Desert Obelisk Key - 0x0332B,0x03367,0x28B8A,0x037B6,0x037B2,0x000F7,0x3351D,0x0053C,0x00771,0x335C8,0x335C9,0x337F8,0x037BB,0x220E4,0x220E5,0x334B9,0x334BC,0x22106,0x0A14C,0x0A14D,0x00359 diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index 272ed176e342..0dbb88a107b1 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: @@ -176,6 +174,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B 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 @@ -753,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: 158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers 158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers -Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp 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: +Swamp 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 @@ -805,7 +804,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 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: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers @@ -980,7 +979,7 @@ Mountainside Obelisk (Mountainside) - Entry - True: 159739 - 0x00367 (Obelisk) - True - True Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: -159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True +159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True 158612 - 0x17C42 (Discard) - True - Triangles 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots Door - 0x00085 (Vault Door) - 0x002A6 @@ -1088,7 +1087,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +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 diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 63e7e36c243e..0f601724acbe 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: @@ -176,6 +174,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B 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 @@ -753,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: 158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers & Rotated Shapers & Triangles & Black/White Squares 158301 - 0x181A9 (Intro Back 8) - 0x00987 - Rotated Shapers & Triangles & Black/White Squares -Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609: 158302 - 0x00609 (Sliding Bridge) - True - Shapers & Black/White Squares 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: +Swamp 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 - Rotated Shapers 158314 - 0x0097F (Platform Row 2) - 0x00982 - Rotated Shapers 158315 - 0x0098F (Platform Row 3) - 0x0097F - Rotated Shapers @@ -805,7 +804,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 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: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Dots & Full Dots 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Dots & Full Dots @@ -980,7 +979,7 @@ Mountainside Obelisk (Mountainside) - Entry - True: 159739 - 0x00367 (Obelisk) - True - True Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: -159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True +159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True 158612 - 0x17C42 (Discard) - True - Arrows 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol Door - 0x00085 (Vault Door) - 0x002A6 @@ -1088,7 +1087,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles & Colored Dots Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +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 diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 1aa9655361f9..f0c6a8690ed3 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: @@ -176,6 +174,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B 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 @@ -753,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: 158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers 158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers -Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: +Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp 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: +Swamp 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 @@ -805,7 +804,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 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: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers @@ -980,7 +979,7 @@ Mountainside Obelisk (Mountainside) - Entry - True: 159739 - 0x00367 (Obelisk) - True - True Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085: -159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True +159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True 158612 - 0x17C42 (Discard) - True - Triangles 158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares Door - 0x00085 (Vault Door) - 0x002A6 @@ -1088,7 +1087,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +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 diff --git a/worlds/witness/data/WitnessLogicVariety.txt b/worlds/witness/data/WitnessLogicVariety.txt new file mode 100644 index 000000000000..b7b705a6db9f --- /dev/null +++ b/worlds/witness/data/WitnessLogicVariety.txt @@ -0,0 +1,1221 @@ +==Tutorial (Inside)== + +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 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 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/settings/Door_Shuffle/Complex_Door_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt index 63d8a58d2676..6c3b328691f9 100644 --- a/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt +++ b/worlds/witness/data/settings/Door_Shuffle/Complex_Door_Panels.txt @@ -9,6 +9,7 @@ Desert Flood Room Entry (Panel) Quarry Entry 1 (Panel) Quarry Entry 2 (Panel) Quarry Stoneworks Entry (Panel) +Quarry Stoneworks Stairs (Panel) Shadows Door Timer (Panel) Keep Hedge Maze 1 (Panel) Keep Hedge Maze 2 (Panel) @@ -28,11 +29,15 @@ Treehouse Third Door (Panel) Treehouse Laser House Door Timer (Panel) Treehouse Drawbridge (Panel) Jungle Popup Wall (Panel) +Jungle Monastery Garden Shortcut (Panel) Bunker Entry (Panel) Bunker Tinted Glass Door (Panel) Swamp Entry (Panel) Swamp Platform Shortcut (Panel) +Swamp Laser Shortcut (Panel) Caves Entry (Panel) +Caves Mountain Shortcut (Panel) +Caves Swamp Shortcut (Panel) Challenge Entry (Panel) Tunnels Entry (Panel) Tunnels Town Shortcut (Panel) \ No newline at end of file diff --git a/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt b/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt deleted file mode 100644 index 78d245f9f0b5..000000000000 --- a/worlds/witness/data/settings/Door_Shuffle/Elevators_Come_To_You.txt +++ /dev/null @@ -1,11 +0,0 @@ -New Connections: -Quarry - Quarry Elevator - TrueOneWay -Outside Quarry - Quarry Elevator - TrueOneWay -Outside Bunker - Bunker Elevator - TrueOneWay -Outside Swamp - Swamp Long Bridge - TrueOneWay -Swamp Near Boat - Swamp Long Bridge - TrueOneWay -Town Red Rooftop - Town Maze Rooftop - TrueOneWay - - -Requirement Changes: -0x035DE - 0x17E2B - True \ No newline at end of file diff --git a/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt b/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt index 23501d20d3a7..f9b8b1b43ae7 100644 --- a/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt +++ b/worlds/witness/data/settings/Door_Shuffle/Simple_Panels.txt @@ -7,6 +7,7 @@ Quarry Stoneworks Panels Quarry Boathouse Panels Keep Hedge Maze Panels Monastery Panels +Jungle Panels Town Church & RGB House Panels Town Maze Panels Windmill & Theater Panels @@ -18,5 +19,4 @@ Mountain Panels Caves Panels Tunnels Panels Glass Factory Entry (Panel) -Shadows Door Timer (Panel) -Jungle Popup Wall (Panel) \ No newline at end of file +Shadows Door Timer (Panel) \ No newline at end of file diff --git a/worlds/witness/data/settings/Early_Caves.txt b/worlds/witness/data/settings/Early_Caves.txt index 48c8056bc7b6..df1e7b114a47 100644 --- a/worlds/witness/data/settings/Early_Caves.txt +++ b/worlds/witness/data/settings/Early_Caves.txt @@ -3,4 +3,8 @@ Caves Shortcuts Remove Items: Caves Mountain Shortcut (Door) -Caves Swamp Shortcut (Door) \ No newline at end of file +Caves Swamp Shortcut (Door) + +Forbidden Doors: +0x021D7 (Caves Mountain Shortcut Panel) +0x17CF2 (Caves Swamp Shortcut Panel) \ No newline at end of file diff --git a/worlds/witness/data/settings/Early_Caves_Start.txt b/worlds/witness/data/settings/Early_Caves_Start.txt index a16a6d02bb9f..bc79007fa54b 100644 --- a/worlds/witness/data/settings/Early_Caves_Start.txt +++ b/worlds/witness/data/settings/Early_Caves_Start.txt @@ -6,4 +6,8 @@ Caves Shortcuts Remove Items: Caves Mountain Shortcut (Door) -Caves Swamp Shortcut (Door) \ No newline at end of file +Caves Swamp Shortcut (Door) + +Forbidden Doors: +0x021D7 (Caves Mountain Shortcut Panel) +0x17CF2 (Caves Swamp Shortcut Panel) \ No newline at end of file diff --git a/worlds/witness/data/settings/Entity_Hunt.txt b/worlds/witness/data/settings/Entity_Hunt.txt new file mode 100644 index 000000000000..4135dbd842f7 --- /dev/null +++ b/worlds/witness/data/settings/Entity_Hunt.txt @@ -0,0 +1,6 @@ +Requirement Changes: +0x03629 - Entity Hunt - True +0x03505 - 0x03629 - True + +New Connections: +Tutorial - Outside Tutorial - True diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index 8eb889f8203a..c64df741982e 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Set, cast from BaseClasses import ItemClassification @@ -7,12 +7,28 @@ from .static_locations import ID_START ITEM_DATA: Dict[str, ItemData] = {} -ITEM_GROUPS: Dict[str, List[str]] = {} +ITEM_GROUPS: Dict[str, Set[str]] = {} # Useful items that are treated specially at generation time and should not be automatically added to the player's # 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(): @@ -22,13 +38,25 @@ def populate_items() -> None: if definition.category is ItemCategory.SYMBOL: classification = ItemClassification.progression - ITEM_GROUPS.setdefault("Symbols", []).append(item_name) + ITEM_GROUPS.setdefault("Symbols", set()).add(item_name) elif definition.category is ItemCategory.DOOR: classification = ItemClassification.progression - ITEM_GROUPS.setdefault("Doors", []).append(item_name) + + first_entity_hex = cast(DoorItemDefinition, definition).panel_id_hexes[0] + entity_type = static_witness_logic.ENTITIES_BY_HEX[first_entity_hex]["entityType"] + + if entity_type == "Door": + ITEM_GROUPS.setdefault("Doors", set()).add(item_name) + elif entity_type == "Panel": + ITEM_GROUPS.setdefault("Panel Keys", set()).add(item_name) + elif entity_type in {"EP", "Obelisk Side", "Obelisk"}: + ITEM_GROUPS.setdefault("Obelisk Keys", set()).add(item_name) + else: + raise ValueError(f"Couldn't figure out what type of door item {definition} is.") + elif definition.category is ItemCategory.LASER: classification = ItemClassification.progression_skip_balancing - ITEM_GROUPS.setdefault("Lasers", []).append(item_name) + ITEM_GROUPS.setdefault("Lasers", set()).add(item_name) elif definition.category is ItemCategory.USEFUL: classification = ItemClassification.useful elif definition.category is ItemCategory.FILLER: @@ -47,7 +75,7 @@ def populate_items() -> None: def get_item_to_door_mappings() -> Dict[int, List[int]]: output: Dict[int, List[int]] = {} for item_name, item_data in ITEM_DATA.items(): - if not isinstance(item_data.definition, DoorItemDefinition): + if not isinstance(item_data.definition, DoorItemDefinition) or item_data.ap_code is None: continue output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] return output diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index e11544235ffc..5c5ad554ddab 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -1,3 +1,5 @@ +from typing import Dict, Set, cast + from . import static_logic as static_witness_logic ID_START = 158000 @@ -102,6 +104,8 @@ "Town RGB House Upstairs Right", "Town RGB House Sound Room Right", + "Town Pet the Dog", + "Windmill Theater Entry Panel", "Theater Exit Left Panel", "Theater Exit Right Panel", @@ -404,6 +408,10 @@ "Mountain Bottom Floor Discard", } +GENERAL_LOCATION_HEXES = { + static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"] for entity_name in GENERAL_LOCATIONS +} + OBELISK_SIDES = { "Desert Obelisk Side 1", "Desert Obelisk Side 2", @@ -441,17 +449,17 @@ "Town Obelisk Side 6", } -ALL_LOCATIONS_TO_ID = dict() +ALL_LOCATIONS_TO_ID: Dict[str, int] = {} -AREA_LOCATION_GROUPS = dict() +AREA_LOCATION_GROUPS: Dict[str, Set[str]] = {} -def get_id(entity_hex: str) -> str: +def get_id(entity_hex: str) -> int: """ Calculates the location ID for any given location """ - return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"] + return cast(int, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"]) def get_event_name(entity_hex: str) -> str: @@ -461,7 +469,7 @@ def get_event_name(entity_hex: str) -> str: action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved" - return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + action + return cast(str, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]) + action ALL_LOCATIONS_TO_IDS = { @@ -479,4 +487,4 @@ def get_event_name(entity_hex: str) -> str: for loc in ALL_LOCATIONS_TO_IDS: area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"] - AREA_LOCATION_GROUPS.setdefault(area, []).append(loc) + AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc) diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index ecd95ea6c0fa..58f2e894e849 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import Dict, List, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple from Utils import cache_argsless @@ -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, @@ -24,13 +25,37 @@ class StaticWitnessLogicObj: - def read_logic_file(self, lines) -> None: + def __init__(self, lines: Optional[List[str]] = None) -> None: + if lines is None: + lines = get_sigma_normal_logic() + + # All regions with a list of panels in them and the connections to other regions, before logic adjustments + self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set)) + self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {} + + self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {} + self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = {} + + self.OBELISK_SIDE_ID_TO_EP_HEXES: Dict[int, Set[int]] = {} + + self.EP_TO_OBELISK_SIDE: Dict[str, str] = {} + + self.ENTITY_ID_TO_NAME: Dict[str, str] = {} + + self.read_logic_file(lines) + self.reverse_connections() + self.combine_connections() + + def read_logic_file(self, lines: List[str]) -> None: """ Reads the logic file and does the initial population of data structures """ - current_region = dict() - current_area = { + current_region = {} + current_area: Dict[str, Any] = { "name": "Misc", "regions": [], } @@ -79,6 +104,7 @@ def read_logic_file(self, lines) -> None: "region": None, "id": None, "entityType": location_id, + "locationType": None, "area": current_area, } @@ -103,19 +129,33 @@ def read_logic_file(self, lines) -> None: "Laser Hedges", "Laser Pressure Plates", } - is_vault_or_video = "Vault" in entity_name or "Video" in entity_name if "Discard" in entity_name: + entity_type = "Panel" location_type = "Discard" - elif is_vault_or_video or entity_name == "Tutorial Gate Close": + elif "Vault" in entity_name: + entity_type = "Panel" location_type = "Vault" elif entity_name in laser_names: - location_type = "Laser" + entity_type = "Laser" + location_type = None elif "Obelisk Side" in entity_name: + entity_type = "Obelisk Side" location_type = "Obelisk Side" + elif "Obelisk" in entity_name: + entity_type = "Obelisk" + location_type = None elif "EP" in entity_name: + entity_type = "EP" location_type = "EP" + elif "Pet the Dog" in entity_name: + entity_type = "Event" + location_type = "Good Boi" + elif entity_hex.startswith("0xFF"): + entity_type = "Event" + location_type = None else: + entity_type = "Panel" location_type = "General" required_items = parse_lambda(required_item_lambda) @@ -128,7 +168,7 @@ def read_logic_file(self, lines) -> None: "items": required_items } - if location_type == "Obelisk Side": + if entity_type == "Obelisk Side": eps = set(next(iter(required_panels))) eps -= {"Theater to Tunnels"} @@ -143,7 +183,8 @@ def read_logic_file(self, lines) -> None: "entity_hex": entity_hex, "region": current_region, "id": int(location_id), - "entityType": location_type, + "entityType": entity_type, + "locationType": location_type, "area": current_area, } @@ -155,7 +196,7 @@ def read_logic_file(self, lines) -> None: current_region["entities"].append(entity_hex) current_region["physical_entities"].append(entity_hex) - def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]): + def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None: target = connection[0] traversal_options = connection[1] @@ -169,13 +210,13 @@ def reverse_connection(self, source_region: str, connection: Tuple[str, Set[Witn if remaining_options: self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options)) - def reverse_connections(self): + def reverse_connections(self) -> None: # Iterate all connections for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()): for connection in connections.items(): self.reverse_connection(region_name, connection) - def combine_connections(self): + def combine_connections(self) -> None: # All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice. self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME} @@ -184,30 +225,6 @@ def combine_connections(self): combined_req = logical_or_witness_rules(requirement) self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req)) - def __init__(self, lines=None) -> None: - if lines is None: - lines = get_sigma_normal_logic() - - # All regions with a list of panels in them and the connections to other regions, before logic adjustments - self.ALL_REGIONS_BY_NAME = dict() - self.ALL_AREAS_BY_NAME = dict() - self.CONNECTIONS_WITH_DUPLICATES = defaultdict(lambda: defaultdict(lambda: set())) - self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() - - self.ENTITIES_BY_HEX = dict() - self.ENTITIES_BY_NAME = dict() - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() - - self.OBELISK_SIDE_ID_TO_EP_HEXES = dict() - - self.EP_TO_OBELISK_SIDE = dict() - - self.ENTITY_ID_TO_NAME = dict() - - self.read_logic_file(lines) - self.reverse_connections() - self.combine_connections() - # Item data parsed from WitnessItems.txt ALL_ITEMS: Dict[str, ItemDefinition] = {} @@ -276,13 +293,20 @@ def get_sigma_expert() -> StaticWitnessLogicObj: return StaticWitnessLogicObj(get_sigma_expert_logic()) -def __getattr__(name): +@cache_argsless +def get_umbra_variety() -> StaticWitnessLogicObj: + return StaticWitnessLogicObj(get_umbra_variety_logic()) + + +def __getattr__(name: str) -> StaticWitnessLogicObj: if name == "vanilla": return get_vanilla() - elif name == "sigma_normal": + if name == "sigma_normal": return get_sigma_normal() - elif name == "sigma_expert": + 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 2934308df3ec..190c00dc283b 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -1,7 +1,9 @@ from math import floor from pkgutil import get_data -from random import random -from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple +from random import Random +from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar + +T = TypeVar("T") # A WitnessRule is just an or-chain of and-conditions. # It represents the set of all options that could fulfill this requirement. @@ -11,9 +13,14 @@ WitnessRule = FrozenSet[FrozenSet[str]] -def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List: +def cast_not_none(value: Optional[T]) -> T: + assert value is not None + return value + + +def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]: positions = range(len(population)) - indices = [] + indices: List[int] = [] while True: needed = k - len(indices) if not needed: @@ -82,13 +89,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str region_obj = { "name": region_name, "shortName": region_name_simple, - "entities": list(), - "physical_entities": list(), + "entities": [], + "physical_entities": [], } return region_obj, options -def parse_lambda(lambda_string) -> WitnessRule: +def parse_lambda(lambda_string: str) -> WitnessRule: """ Turns a lambda String literal like this: a | b & c into a set of sets like this: {{a}, {b, c}} @@ -97,18 +104,18 @@ def parse_lambda(lambda_string) -> WitnessRule: if lambda_string == "True": return frozenset([frozenset()]) split_ands = set(lambda_string.split(" | ")) - lambda_set = frozenset({frozenset(a.split(" & ")) for a in split_ands}) + return frozenset({frozenset(a.split(" & ")) for a in split_ands}) - return lambda_set - -_adjustment_file_cache = dict() +_adjustment_file_cache = {} def get_adjustment_file(adjustment_file: str) -> List[str]: if adjustment_file not in _adjustment_file_cache: - data = get_data(__name__, adjustment_file).decode("utf-8") - _adjustment_file_cache[adjustment_file] = [line.strip() for line in data.split("\n")] + data = get_data(__name__, adjustment_file) + if data is None: + raise FileNotFoundError(f"Could not find {adjustment_file}") + _adjustment_file_cache[adjustment_file] = [line.strip() for line in data.decode("utf-8").split("\n")] return _adjustment_file_cache[adjustment_file] @@ -197,8 +204,8 @@ def get_caves_except_path_to_challenge_exclusion_list() -> List[str]: return get_adjustment_file("settings/Exclusions/Caves_Except_Path_To_Challenge.txt") -def get_elevators_come_to_you() -> List[str]: - return get_adjustment_file("settings/Door_Shuffle/Elevators_Come_To_You.txt") +def get_entity_hunt() -> List[str]: + return get_adjustment_file("settings/Entity_Hunt.txt") def get_sigma_normal_logic() -> List[str]: @@ -209,6 +216,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") @@ -237,7 +248,7 @@ def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRu A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d". These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b. """ - current_overall_requirement = frozenset({frozenset()}) + current_overall_requirement: FrozenSet[FrozenSet[str]] = frozenset({frozenset()}) for next_dnf_requirement in witness_rules: new_requirement: Set[FrozenSet[str]] = set() diff --git a/worlds/witness/entity_hunt.py b/worlds/witness/entity_hunt.py new file mode 100644 index 000000000000..9549246ce479 --- /dev/null +++ b/worlds/witness/entity_hunt.py @@ -0,0 +1,268 @@ +from collections import defaultdict +from logging import debug, warning +from pprint import pformat +from typing import TYPE_CHECKING, Dict, List, Set, Tuple + +from .data import static_logic as static_witness_logic + +if TYPE_CHECKING: + from . import WitnessWorld + from .player_logic import WitnessPlayerLogic + +DISALLOWED_ENTITIES_FOR_PANEL_HUNT = { + "0x03629", # Tutorial Gate Open, which is the panel that is locked by panel hunt + "0x03505", # Tutorial Gate Close (same thing) + "0x3352F", # Gate EP (same thing) + "0x09F7F", # Mountaintop Box Short. This is reserved for panel_hunt_postgame. + "0x00CDB", # Challenge Reallocating + "0x0051F", # Challenge Reallocating + "0x00524", # Challenge Reallocating + "0x00CD4", # Challenge Reallocating + "0x00CB9", # Challenge May Be Unsolvable + "0x00CA1", # Challenge May Be Unsolvable + "0x00C80", # Challenge May Be Unsolvable + "0x00C68", # Challenge May Be Unsolvable + "0x00C59", # Challenge May Be Unsolvable + "0x00C22", # Challenge May Be Unsolvable + "0x0A3A8", # Reset PP + "0x0A3B9", # Reset PP + "0x0A3BB", # Reset PP + "0x0A3AD", # Reset PP +} + +ALL_HUNTABLE_PANELS = [ + entity_hex + for entity_hex, entity_obj in static_witness_logic.ENTITIES_BY_HEX.items() + if entity_obj["entityType"] == "Panel" and entity_hex not in DISALLOWED_ENTITIES_FOR_PANEL_HUNT +] + + +class EntityHuntPicker: + def __init__(self, player_logic: "WitnessPlayerLogic", world: "WitnessWorld", + pre_picked_entities: Set[str]) -> None: + self.player_logic = player_logic + self.player_options = world.options + self.player_name = world.player_name + self.random = world.random + + self.PRE_PICKED_HUNT_ENTITIES = pre_picked_entities.copy() + self.HUNT_ENTITIES: Set[str] = set() + + self._add_plandoed_hunt_panels_to_pre_picked() + + self.ALL_ELIGIBLE_ENTITIES, self.ELIGIBLE_ENTITIES_PER_AREA = self._get_eligible_panels() + + def pick_panel_hunt_panels(self, total_amount: int) -> Set[str]: + """ + The process of picking all hunt entities is: + + 1. Add pre-defined hunt entities + 2. Pick random hunt entities to fill out the rest + 3. Replace unfair entities with fair entities + + Each of these is its own function. + """ + + self.HUNT_ENTITIES = self.PRE_PICKED_HUNT_ENTITIES.copy() + + self._pick_all_hunt_entities(total_amount) + self._replace_unfair_hunt_entities_with_good_hunt_entities() + self._log_results() + + return self.HUNT_ENTITIES + + def _entity_is_eligible(self, panel_hex: str, plando: bool = False) -> bool: + """ + Determine whether an entity is eligible for entity hunt based on player options. + """ + panel_obj = static_witness_logic.ENTITIES_BY_HEX[panel_hex] + + if not self.player_logic.solvability_guaranteed(panel_hex) or panel_hex in self.player_logic.EXCLUDED_ENTITIES: + if plando: + warning(f"Panel {panel_obj['checkName']} is disabled / excluded and thus not eligible for panel hunt.") + return False + + return plando or not ( + # Due to an edge case, Discards have to be on in disable_non_randomized even if Discard Shuffle is off. + # However, I don't think they should be hunt panels in this case. + self.player_options.disable_non_randomized_puzzles + and not self.player_options.shuffle_discarded_panels + and panel_obj["locationType"] == "Discard" + ) + + def _add_plandoed_hunt_panels_to_pre_picked(self) -> None: + """ + Add panels the player explicitly specified to be included in panel hunt to the pre picked hunt panels. + Output a warning if a panel could not be added for some reason. + """ + + # Plandoed hunt panels should be in random order, but deterministic by seed, so we sort, then shuffle + panels_to_plando = sorted(self.player_options.panel_hunt_plando.value) + self.random.shuffle(panels_to_plando) + + for location_name in panels_to_plando: + entity_hex = static_witness_logic.ENTITIES_BY_NAME[location_name]["entity_hex"] + + if entity_hex in self.PRE_PICKED_HUNT_ENTITIES: + continue + + if self._entity_is_eligible(entity_hex, plando=True): + if len(self.PRE_PICKED_HUNT_ENTITIES) == self.player_options.panel_hunt_total: + warning( + f"Panel {location_name} could not be plandoed as a hunt panel for {self.player_name}'s world, " + f"because it would exceed their panel hunt total." + ) + continue + + self.PRE_PICKED_HUNT_ENTITIES.add(entity_hex) + + def _get_eligible_panels(self) -> Tuple[List[str], Dict[str, Set[str]]]: + """ + There are some entities that are not allowed for panel hunt for various technical of gameplay reasons. + Make a list of all the ones that *are* eligible, plus a lookup of eligible panels per area. + """ + + all_eligible_panels = [ + panel for panel in ALL_HUNTABLE_PANELS + if self._entity_is_eligible(panel) + ] + + eligible_panels_by_area = defaultdict(set) + for eligible_panel in all_eligible_panels: + associated_area = static_witness_logic.ENTITIES_BY_HEX[eligible_panel]["area"]["name"] + eligible_panels_by_area[associated_area].add(eligible_panel) + + return all_eligible_panels, eligible_panels_by_area + + def _get_percentage_of_hunt_entities_by_area(self) -> Dict[str, float]: + hunt_entities_picked_so_far_prevent_div_0 = max(len(self.HUNT_ENTITIES), 1) + + contributing_percentage_per_area = {} + for area, eligible_entities in self.ELIGIBLE_ENTITIES_PER_AREA.items(): + amount_of_already_chosen_entities = len(self.ELIGIBLE_ENTITIES_PER_AREA[area] & self.HUNT_ENTITIES) + current_percentage = amount_of_already_chosen_entities / hunt_entities_picked_so_far_prevent_div_0 + contributing_percentage_per_area[area] = current_percentage + + return contributing_percentage_per_area + + def _get_next_random_batch(self, amount: int, same_area_discouragement: float) -> List[str]: + """ + Pick the next batch of hunt entities. + Areas that already have a lot of hunt entities in them will be discouraged from getting more. + The strength of this effect is controlled by the same_area_discouragement factor from the player's options. + """ + + percentage_of_hunt_entities_by_area = self._get_percentage_of_hunt_entities_by_area() + + max_percentage = max(percentage_of_hunt_entities_by_area.values()) + if max_percentage == 0: + allowance_per_area = {area: 1.0 for area in percentage_of_hunt_entities_by_area} + else: + allowance_per_area = { + area: (max_percentage - current_percentage) / max_percentage + for area, current_percentage in percentage_of_hunt_entities_by_area.items() + } + # use same_area_discouragement as lerp factor + allowance_per_area = { + area: (1.0 - same_area_discouragement) + (weight * same_area_discouragement) + for area, weight in allowance_per_area.items() + } + + assert min(allowance_per_area.values()) >= 0, ( + f"Somehow, an area had a negative weight when picking hunt entities: {allowance_per_area}" + ) + + remaining_entities, remaining_entity_weights = [], [] + for area, eligible_entities in self.ELIGIBLE_ENTITIES_PER_AREA.items(): + for panel in sorted(eligible_entities - self.HUNT_ENTITIES): + remaining_entities.append(panel) + remaining_entity_weights.append(allowance_per_area[area]) + + # I don't think this can ever happen, but let's be safe + if sum(remaining_entity_weights) == 0: + remaining_entity_weights = [1] * len(remaining_entity_weights) + + return self.random.choices(remaining_entities, weights=remaining_entity_weights, k=amount) + + def _pick_all_hunt_entities(self, total_amount: int) -> None: + """ + The core function of the EntityHuntPicker in which all Hunt Entities are picked, + respecting the player's choices for total amount and same area discouragement. + """ + same_area_discouragement = self.player_options.panel_hunt_discourage_same_area_factor / 100 + + # If we're using random picking, just choose all the entities now and return + if not same_area_discouragement: + hunt_entities = self.random.sample( + [entity for entity in self.ALL_ELIGIBLE_ENTITIES if entity not in self.HUNT_ENTITIES], + k=total_amount - len(self.HUNT_ENTITIES), + ) + self.HUNT_ENTITIES.update(hunt_entities) + return + + # If we're discouraging entities from the same area being picked, we have to pick entities one at a time + # For higher total counts, we do them in small batches for performance + batch_size = max(1, total_amount // 20) + + while len(self.HUNT_ENTITIES) < total_amount: + actual_amount_to_pick = min(batch_size, total_amount - len(self.HUNT_ENTITIES)) + + self.HUNT_ENTITIES.update(self._get_next_random_batch(actual_amount_to_pick, same_area_discouragement)) + + def _replace_unfair_hunt_entities_with_good_hunt_entities(self) -> None: + """ + For connected entities that "solve together", make sure that the one you're guaranteed + to be able to see and interact with first is the one that is chosen, so you don't get "surprise entities". + """ + + replacements = { + "0x18488": "0x00609", # Replace Swamp Sliding Bridge Underwater -> Swamp Sliding Bridge Above Water + "0x03676": "0x03678", # Replace Quarry Upper Ramp Control -> Lower Ramp Control + "0x03675": "0x03679", # Replace Quarry Upper Lift Control -> Lower Lift Control + + "0x03702": "0x15ADD", # Jungle Vault Box -> Jungle Vault Panel + "0x03542": "0x002A6", # Mountainside Vault Box -> Mountainside Vault Panel + "0x03481": "0x033D4", # Tutorial Vault Box -> Tutorial Vault Panel + "0x0339E": "0x0CC7B", # Desert Vault Box -> Desert Vault Panel + "0x03535": "0x00AFB", # Shipwreck Vault Box -> Shipwreck Vault Panel + } + + if self.player_options.shuffle_doors < 2: + replacements.update( + { + "0x334DC": "0x334DB", # In door shuffle, the Shadows Timer Panels are disconnected + "0x17CBC": "0x2700B", # In door shuffle, the Laser Timer Panels are disconnected + } + ) + + for bad_entitiy, good_entity in replacements.items(): + # If the bad entity was picked as a hunt entity ... + if bad_entitiy not in self.HUNT_ENTITIES: + continue + + # ... and the good entity was not ... + if good_entity in self.HUNT_ENTITIES or good_entity not in self.ALL_ELIGIBLE_ENTITIES: + continue + + # ... and it's not a forced pick that should stay the same ... + if bad_entitiy in self.PRE_PICKED_HUNT_ENTITIES: + continue + + # ... replace the bad entity with the good entity. + self.HUNT_ENTITIES.remove(bad_entitiy) + self.HUNT_ENTITIES.add(good_entity) + + def _log_results(self) -> None: + final_percentage_by_area = self._get_percentage_of_hunt_entities_by_area() + + sorted_area_percentages_dict = dict(sorted(final_percentage_by_area.items(), key=lambda x: x[1])) + sorted_area_percentages_dict_pretty_print = { + area: str(percentage) + (" (maxed)" if self.ELIGIBLE_ENTITIES_PER_AREA[area] <= self.HUNT_ENTITIES else "") + for area, percentage in sorted_area_percentages_dict.items() + } + player_name = self.player_name + discouragemenet_factor = self.player_options.panel_hunt_discourage_same_area_factor + debug( + f'Final area percentages for player "{player_name}" ({discouragemenet_factor} discouragement):\n' + f"{pformat(sorted_area_percentages_dict_pretty_print)}" + ) diff --git a/worlds/witness/generate_data_file.py b/worlds/witness/generate_data_file.py new file mode 100644 index 000000000000..50a63a374619 --- /dev/null +++ b/worlds/witness/generate_data_file.py @@ -0,0 +1,45 @@ +from collections import defaultdict + +from data import static_logic as static_witness_logic + +if __name__ == "__main__": + with open("data/APWitnessData.h", "w") as datafile: + datafile.write("""# pragma once + +# include +# include +# include + +""") + + area_to_location_ids = defaultdict(list) + area_to_entity_ids = defaultdict(list) + + for entity_id, entity_object in static_witness_logic.ENTITIES_BY_HEX.items(): + location_id = entity_object["id"] + + area = entity_object["area"]["name"] + area_to_entity_ids[area].append(entity_id) + + if location_id is None: + continue + + area_to_location_ids[area].append(str(location_id)) + + datafile.write("inline std::map> areaNameToLocationIDs = {\n") + datafile.write( + "\n".join( + '\t{"' + area + '", { ' + ", ".join(location_ids) + " }}," + for area, location_ids in area_to_location_ids.items() + ) + ) + datafile.write("\n};\n\n") + + datafile.write("inline std::map> areaNameToEntityIDs = {\n") + datafile.write( + "\n".join( + '\t{"' + area + '", { ' + ", ".join(entity_ids) + " }}," + for area, entity_ids in area_to_entity_ids.items() + ) + ) + datafile.write("\n};\n\n") diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 535a36e13b6f..82837aed0686 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -1,16 +1,19 @@ import logging +import math from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast -from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld +from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region from .data import static_logic as static_witness_logic from .data.utils import weighted_sample +from .player_items import WitnessItem if TYPE_CHECKING: from . import WitnessWorld -CompactItemData = Tuple[str, Union[str, int], int] +CompactHintArgs = Tuple[Union[str, int], Union[str, int]] +CompactHintData = Tuple[str, Union[str, int], Union[str, int]] @dataclass @@ -22,7 +25,9 @@ class WitnessLocationHint: def __hash__(self) -> int: return hash(self.location) - def __eq__(self, other) -> bool: + def __eq__(self, other: Any) -> bool: + if not isinstance(other, WitnessLocationHint): + return False return self.location == other.location @@ -32,6 +37,8 @@ class WitnessWordedHint: location: Optional[Location] = None area: Optional[str] = None area_amount: Optional[int] = None + area_hunt_panels: Optional[int] = None + vague_location_hint: bool = False def get_always_hint_items(world: "WitnessWorld") -> List[str]: @@ -46,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") @@ -165,32 +172,112 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]: return priority +def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Location) -> Tuple[str, str]: + allow_regions = world.options.vague_hints == "experimental" + + possible_location_groups = { + group_name: group_locations + for group_name, group_locations in world.multiworld.worlds[hint_loc.player].location_name_groups.items() + if hint_loc.name in group_locations + } + + locations_in_that_world = { + location.name for location in world.multiworld.get_locations(hint_loc.player) if not location.is_event + } + + valid_location_groups: Dict[str, int] = {} + + # Find valid location groups. + for group, locations in possible_location_groups.items(): + if group == "Everywhere": + continue + present_locations = sum(location in locations_in_that_world for location in locations) + valid_location_groups[group] = present_locations + + # If there are valid location groups, use a random one. + if valid_location_groups: + # If there are location groups with more than 1 location, remove any that only have 1. + if any(num_locs > 1 for num_locs in valid_location_groups.values()): + valid_location_groups = {name: num_locs for name, num_locs in valid_location_groups.items() if num_locs > 1} + + location_groups_with_weights = { + # Listen. Just don't worry about it. :))) + location_group: (x ** 0.6) * math.e ** (- (x / 7) ** 0.6) if x > 6 else x / 6 + for location_group, x in valid_location_groups.items() + } + + location_groups = list(location_groups_with_weights.keys()) + weights = list(location_groups_with_weights.values()) + + return world.random.choices(location_groups, weights, k=1)[0], "Group" + + if allow_regions: + return cast(Region, hint_loc.parent_region).name, "Region" + + return "Everywhere", "Everywhere" + + def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item - item_name = item.name - if item.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item.player) + ")" - if hint.hint_came_from_location: - hint_text = f"{location_name} contains {item_name}." - else: - hint_text = f"{item_name} can be found at {location_name}." + item_name = "Nothing" + if item is not None: + item_name = item.name + + if item.player != world.player: + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" + + hint_text = "" + area: Optional[str] = None - return WitnessWordedHint(hint_text, hint.location) + if world.options.vague_hints: + chosen_group, group_type = try_getting_location_group_for_location(world, hint.location) + if hint.location.player == world.player: + area = chosen_group -def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]) -> Optional[WitnessLocationHint]: - def get_real_location(multiworld: MultiWorld, location: Location): + # local locations should only ever return a location group, as Witness defines groups for every location. + hint_text = f"{item_name} can be found in the {area} area." + else: + player_name = world.multiworld.get_player_name(hint.location.player) + + if group_type == "Everywhere": + location_name = f"a location in {player_name}'s world" + elif group_type == "Group": + location_name = f"a \"{chosen_group}\" location in {player_name}'s world" + elif group_type == "Region": + origin_region_name = world.multiworld.worlds[hint.location.player].origin_region_name + if chosen_group == origin_region_name: + location_name = ( + f"a location in the origin region of {player_name}'s world (\"{origin_region_name}\" region)" + ) + else: + location_name = f"a location in {player_name}'s \"{chosen_group}\" region" + + if hint_text == "": + if hint.hint_came_from_location: + hint_text = f"{location_name} contains {item_name}." + else: + hint_text = f"{item_name} can be found at {location_name}." + + return WitnessWordedHint(hint_text, hint.location, area=area, vague_location_hint=bool(world.options.vague_hints)) + + +def hint_from_item(world: "WitnessWorld", item_name: str, + own_itempool: List["WitnessItem"]) -> Optional[WitnessLocationHint]: + def get_real_location(multiworld: MultiWorld, location: Location) -> Location: """If this location is from an item_link pseudo-world, get the location that the item_link item is on. Return the original location otherwise / as a fallback.""" if location.player not in world.multiworld.groups: return location try: + if not location.item: + return location return multiworld.find_item(location.item.name, location.player) except StopIteration: return location @@ -209,54 +296,60 @@ def get_real_location(multiworld: MultiWorld, location: Location): def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]: - location_obj = world.get_location(location) - item_obj = location_obj.item - item_name = item_obj.name - if item_obj.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")" - - return WitnessLocationHint(location_obj, True) + return WitnessLocationHint(world.get_location(location), True) -def get_items_and_locations_in_random_order(world: "WitnessWorld", - own_itempool: List[Item]) -> Tuple[List[str], List[str]]: - prog_items_in_this_world = sorted( +def get_item_and_location_names_in_random_order(world: "WitnessWorld", + own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]: + progression_item_names_in_this_world = [ item.name for item in own_itempool if item.advancement and item.code and item.location - ) - locations_in_this_world = sorted( - location.name for location in world.multiworld.get_locations(world.player) - if location.address and location.progress_type != LocationProgressType.EXCLUDED - ) + ] + world.random.shuffle(progression_item_names_in_this_world) - world.random.shuffle(prog_items_in_this_world) + locations_in_this_world = [ + location for location in world.multiworld.get_locations(world.player) + if location.item and not location.is_event and location.progress_type != LocationProgressType.EXCLUDED + ] world.random.shuffle(locations_in_this_world) - return prog_items_in_this_world, locations_in_this_world + if world.options.vague_hints: + locations_in_this_world.sort(key=lambda location: cast(Item, location.item).advancement) + + location_names_in_this_world = [location.name for location in locations_in_this_world] + return progression_item_names_in_this_world, location_names_in_this_world -def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Item], + +def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location] ) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]: - prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool) - always_locations = [ - location for location in get_always_hint_locations(world) - if location in loc_in_this_world - ] + progression_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order( + world, own_itempool + ) + always_items = [ item for item in get_always_hint_items(world) - if item in prog_items_in_this_world - ] - priority_locations = [ - location for location in get_priority_hint_locations(world) - if location in loc_in_this_world + if item in progression_items_in_this_world ] priority_items = [ item for item in get_priority_hint_items(world) - if item in prog_items_in_this_world + if item in progression_items_in_this_world ] + if world.options.vague_hints: + always_locations, priority_locations = [], [] + else: + always_locations = [ + location for location in get_always_hint_locations(world) + if location in locations_in_this_world + ] + priority_locations = [ + location for location in get_priority_hint_locations(world) + if location in locations_in_this_world + ] + # Get always and priority location/item hints always_location_hints = {hint_from_location(world, location) for location in always_locations} always_item_hints = {hint_from_item(world, item, own_itempool) for item in always_items} @@ -282,14 +375,16 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Ite return always_hints, priority_hints -def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item], +def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint], unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]: - prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool) + progression_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order( + world, own_itempool + ) next_random_hint_is_location = world.random.randrange(0, 2) - hints = [] + hints: List[WitnessWordedHint] = [] # This is a way to reverse a Dict[a,List[b]] to a Dict[b,a] area_reverse_lookup = { @@ -299,17 +394,17 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp } while len(hints) < hint_amount: - if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Ran out of items/locations to hint for player {player_name}.") + if not progression_items_in_this_world and not locations_in_this_world and not hints_to_use_first: + logging.warning(f"Ran out of items/locations to hint for player {world.player_name}.") break + location_hint: Optional[WitnessLocationHint] if hints_to_use_first: location_hint = hints_to_use_first.pop() elif next_random_hint_is_location and locations_in_this_world: location_hint = hint_from_location(world, locations_in_this_world.pop()) - elif not next_random_hint_is_location and prog_items_in_this_world: - location_hint = hint_from_item(world, prog_items_in_this_world.pop(), own_itempool) + elif not next_random_hint_is_location and progression_items_in_this_world: + location_hint = hint_from_item(world, progression_items_in_this_world.pop(), own_itempool) # The list that the hint was supposed to be taken from was empty. # Try the other list, which has to still have something, as otherwise, all lists would be empty, # which would have triggered the guard condition above. @@ -317,7 +412,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp next_random_hint_is_location = not next_random_hint_is_location continue - if not location_hint or location_hint.location in already_hinted_locations: + if location_hint is None or location_hint.location in already_hinted_locations: continue # Don't hint locations in areas that are almost fully hinted out already @@ -344,8 +439,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st When this happens, they are made less likely to receive an area hint. """ - unhinted_locations_per_area = dict() - unhinted_location_percentage_per_area = dict() + unhinted_locations_per_area = {} + unhinted_location_percentage_per_area = {} for area_name, locations in locations_per_area.items(): not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations) @@ -368,8 +463,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]: potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys()) - locations_per_area = dict() - items_per_area = dict() + locations_per_area = {} + items_per_area = {} for area in potential_areas: regions = [ @@ -377,7 +472,7 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]] for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"] if region in world.player_regions.created_region_names ] - locations = [location for region in regions for location in region.get_locations() if location.address] + locations = [location for region in regions for location in region.get_locations() if not location.is_event] if locations: locations_per_area[area] = locations @@ -386,22 +481,22 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]] return locations_per_area, items_per_area -def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items: List[Item]) -> Tuple[str, int]: +def word_area_hint(world: "WitnessWorld", hinted_area: str, area_items: List[Item]) -> Tuple[str, int, Optional[int]]: """ Word the hint for an area using natural sounding language. This takes into account how much progression there is, how much of it is local/non-local, and whether there are any local lasers to be found in this area. """ - local_progression = sum(item.player == world.player and item.advancement for item in corresponding_items) - non_local_progression = sum(item.player != world.player and item.advancement for item in corresponding_items) + local_progression = sum(item.player == world.player and item.advancement for item in area_items) + non_local_progression = sum(item.player != world.player and item.advancement for item in area_items) laser_names = {"Symmetry Laser", "Desert Laser", "Quarry Laser", "Shadows Laser", "Town Laser", "Monastery Laser", "Jungle Laser", "Bunker Laser", "Swamp Laser", "Treehouse Laser", "Keep Laser", } local_lasers = sum( item.player == world.player and item.name in laser_names - for item in corresponding_items + for item in area_items ) total_progression = non_local_progression + local_progression @@ -410,11 +505,29 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items: area_progression_word = "Both" if total_progression == 2 else "All" + hint_string = f"In the {hinted_area} area, you will find " + + hunt_panels = None + if world.options.victory_condition == "panel_hunt": + hunt_panels = sum( + static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"]["name"] == hinted_area + for hunt_entity in world.player_logic.HUNT_ENTITIES + ) + + if not hunt_panels: + hint_string += "no Hunt Panels and " + + elif hunt_panels == 1: + hint_string += "1 Hunt Panel and " + + else: + hint_string += f"{hunt_panels} Hunt Panels and " + if not total_progression: - hint_string = f"In the {hinted_area} area, you will find no progression items." + hint_string += "no progression items." elif total_progression == 1: - hint_string = f"In the {hinted_area} area, you will find 1 progression item." + hint_string += "1 progression item." if player_count > 1: if local_lasers: @@ -429,7 +542,7 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items: hint_string += "\nThis item is a laser." else: - hint_string = f"In the {hinted_area} area, you will find {total_progression} progression items." + hint_string += f"{total_progression} progression items." if local_lasers == total_progression: sentence_end = (" for this world." if player_count > 1 else ".") @@ -466,7 +579,7 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items: elif local_lasers: hint_string += f"\n{local_lasers} of them are lasers." - return hint_string, total_progression + return hint_string, total_progression, hunt_panels def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations: Set[Location] @@ -478,13 +591,14 @@ def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations hints = [] for hinted_area in hinted_areas: - hint_string, prog_amount = word_area_hint(world, hinted_area, items_per_area[hinted_area]) + hint_string, progression_amount, hunt_panels = word_area_hint(world, hinted_area, items_per_area[hinted_area]) - hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount)) + hints.append( + WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", progression_amount, hunt_panels) + ) if len(hinted_areas) < amount: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Was not able to make {amount} area hints for player {player_name}. " + logging.warning(f"Was not able to make {amount} area hints for player {world.player_name}. " f"Made {len(hinted_areas)} instead, and filled the rest with random location hints.") return hints, unhinted_locations_per_area @@ -533,7 +647,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int, location_hints_created_in_round_1 = len(generated_hints) - unhinted_locations_per_area: Dict[str, Set[Location]] = dict() + unhinted_locations_per_area: Dict[str, Set[Location]] = {} # Then, make area hints. if area_hints: @@ -573,28 +687,53 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int, # If we still don't have enough for whatever reason, throw a warning, proceed with the lower amount if len(generated_hints) != hint_amount: - player_name = world.multiworld.get_player_name(world.player) - logging.warning(f"Couldn't generate {hint_amount} hints for player {player_name}. " + logging.warning(f"Couldn't generate {hint_amount} hints for player {world.player_name}. " f"Generated {len(generated_hints)} instead.") return generated_hints -def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) -> CompactItemData: +def get_compact_hint_args(hint: WitnessWordedHint, local_player_number: int) -> CompactHintArgs: + """ + Arg reference: + + Area Hint: 1st Arg is the amount of area progression and hunt panels. 2nd Arg is the name of the area. + Location Hint: 1st Arg is the location's address, second arg is the player number the location belongs to. + Junk Hint: 1st Arg is -1, second arg is this slot's player number. + """ + + # Is Area Hint + if hint.area_amount is not None: + area_amount = hint.area_amount + hunt_panels = hint.area_hunt_panels + + area_and_hunt_panels = area_amount + # Encode amounts together + if hunt_panels: + area_and_hunt_panels += 0x100 * hunt_panels + + return hint.area, area_and_hunt_panels + location = hint.location - area_amount = hint.area_amount - # None if junk hint, address if location hint, area string if area hint - arg_1 = location.address if location else (hint.area if hint.area else None) + # Is location hint + if location and location.address is not None: + if hint.vague_location_hint and location.player == local_player_number: + assert hint.area is not None # A local vague location hint should have an area argument + return location.address, "containing_area:" + hint.area + return location.address, location.player # Scouting does not matter for other players (currently) + + # Is junk / undefined hint + return -1, local_player_number - # self.player if junk hint, player if location hint, progression amount if area hint - arg_2 = area_amount if area_amount is not None else (location.player if location else local_player_number) - return hint.wording, arg_1, arg_2 +def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) -> CompactHintData: + compact_arg_1, compact_arg_2 = get_compact_hint_args(hint, local_player_number) + return hint.wording, compact_arg_1, compact_arg_2 def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]: - laser_hints_by_name = dict() + laser_hints_by_name = {} for item_name in laser_names: location_hint = hint_from_item(world, item_name, world.own_itempool) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index df8214ac9221..49a4437c5ab7 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -19,7 +19,7 @@ class WitnessPlayerLocations: def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None: """Defines locations AFTER logic changes due to options""" - self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} + self.PANEL_TYPES_TO_SHUFFLE = {"General", "Good Boi"} self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy() if world.options.shuffle_discarded_panels: @@ -44,30 +44,22 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] - for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS + for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES } self.CHECK_PANELHEX_TO_ID = { static_witness_logic.ENTITIES_BY_NAME[ch]["entity_hex"]: static_witness_locations.ALL_LOCATIONS_TO_ID[ch] for ch in self.CHECK_LOCATIONS - if static_witness_logic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE + if static_witness_logic.ENTITIES_BY_NAME[ch]["locationType"] in self.PANEL_TYPES_TO_SHUFFLE } - dog_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"] - dog_id = static_witness_locations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] - self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id - self.CHECK_PANELHEX_TO_ID = dict( sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1]) ) - event_locations = { - p for p in player_logic.USED_EVENT_NAMES_BY_HEX - } - self.EVENT_LOCATION_TABLE = { - static_witness_locations.get_event_name(entity_hex): None - for entity_hex in event_locations + event_location: None + for event_location in player_logic.EVENT_ITEM_PAIRS } check_dict = { @@ -80,5 +72,5 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N def add_location_late(self, entity_name: str) -> None: entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"] - self.CHECK_LOCATION_TABLE[entity_hex] = entity_name + self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex) self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index f51d86ba22f3..d739517870a5 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -2,10 +2,23 @@ from schema import And, Schema -from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle +from Options import ( + Choice, + DefaultOnToggle, + LocationSet, + OptionDict, + OptionError, + OptionGroup, + OptionSet, + PerGameCommonOptions, + Range, + Toggle, + Visibility, +) from .data import static_logic as static_witness_logic from .data.item_definition_classes import ItemCategory, WeightedItemDefinition +from .entity_hunt import ALL_HUNTABLE_PANELS class DisableNonRandomizedPuzzles(Toggle): @@ -35,6 +48,14 @@ class EarlyCaves(Choice): alias_on = 2 +class EarlySymbolItem(DefaultOnToggle): + """ + Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early. + """ + + visibility = Visibility.none + + class ShuffleSymbols(DefaultOnToggle): """ If on, you will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols. @@ -121,12 +142,18 @@ class ShuffleEnvironmentalPuzzles(Choice): option_obelisk_sides = 2 -class ShuffleDog(Toggle): +class ShuffleDog(Choice): """ - Adds petting the Town dog into the location pool. + Adds petting the dog statue in Town into the location pool. + Alternatively, you can force it to be a Puzzle Skip. """ display_name = "Pet the Dog" + option_off = 0 + option_puzzle_skip = 1 + option_random_item = 2 + default = 1 + class EnvironmentalPuzzlesDifficulty(Choice): """ @@ -150,6 +177,16 @@ class ObeliskKeys(DefaultOnToggle): display_name = "Obelisk Keys" +class UnlockableWarps(Toggle): + """ + Adds unlockable fast travel points to the game. + These warp points are represented by spheres in game. You walk up to one, you unlock it for warping. + + The warp points are: Entry, Symmetry Island, Desert, Quarry, Keep, Shipwreck, Town, Jungle, Bunker, Treehouse, Mountaintop, Caves. + """ + display_name = "Unlockable Fast Travel Points" + + class ShufflePostgame(Toggle): """ Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal. @@ -165,6 +202,7 @@ class VictoryCondition(Choice): - Challenge: Beat the secret Challenge (requires Challenge Lasers). - Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers). - Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers). + - Panel Hunt: Solve a specific number of randomly selected panels before going to the secret ending in Tutorial. It is important to note that while the Mountain Box requires Desert Laser to be redirected in Town for that laser to count, the laser locks on the Elevator and Challenge Timer panels do not. @@ -174,15 +212,86 @@ class VictoryCondition(Choice): option_challenge = 1 option_mountain_box_short = 2 option_mountain_box_long = 3 + option_panel_hunt = 4 + + +class PanelHuntTotal(Range): + """ + Sets the number of random panels that will get marked as "Panel Hunt" panels in the "Panel Hunt" game mode. + """ + display_name = "Total Panel Hunt panels" + range_start = 5 + range_end = 100 + default = 40 + + +class PanelHuntRequiredPercentage(Range): + """ + Determines the percentage of "Panel Hunt" panels that need to be solved to win. + """ + display_name = "Percentage of required Panel Hunt panels" + range_start = 20 + range_end = 100 + default = 63 + + +class PanelHuntPostgame(Choice): + """ + In panel hunt, there are technically no postgame locations. + Depending on your options, this can leave Mountain and Caves as two huge areas with Hunt Panels in them that cannot be reached until you get enough lasers to go through the very linear Mountain descent. + Panel Hunt tends to be more fun when the world is open. + This option lets you force anything locked by lasers to be disabled, and thus ineligible for Hunt Panels. + To compensate, the respective mountain box solution (short box / long box) will be forced to be a Hunt Panel. + Does nothing if Panel Hunt is not your victory condition. + + Note: The "Mountain Lasers" option may also affect locations locked by challenge lasers if the only path to those locations leads through the Mountain Entry. + """ + + display_name = "Force postgame in Panel Hunt" + + option_everything_is_eligible = 0 + option_disable_mountain_lasers_locations = 1 + option_disable_challenge_lasers_locations = 2 + option_disable_anything_locked_by_lasers = 3 + default = 3 + + +class PanelHuntDiscourageSameAreaFactor(Range): + """ + The greater this value, the less likely it is that many Hunt Panels show up in the same area. + + At 0, Hunt Panels will be selected randomly. + At 100, Hunt Panels will be almost completely evenly distributed between areas. + """ + display_name = "Panel Hunt Discourage Same Area Factor" + + range_start = 0 + range_end = 100 + default = 40 + + +class PanelHuntPlando(LocationSet): + """ + Specify specific hunt panels you want for your panel hunt game. + """ + + display_name = "Panel Hunt Plando" + + valid_keys = [static_witness_logic.ENTITIES_BY_HEX[panel_hex]["checkName"] for panel_hex in ALL_HUNTABLE_PANELS] 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 @@ -208,12 +317,33 @@ class ChallengeLasers(Range): default = 11 -class ElevatorsComeToYou(Toggle): +class ElevatorsComeToYou(OptionSet): """ - If on, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them. - This does actually affect logic as it allows unintended backwards / early access into these areas. + In vanilla, some bridges/elevators come to you if you walk up to them when they are not currently there. + However, there are some that don't. Notably, this prevents Quarry Elevator from being a logical access method into Quarry, because you can send it away without riding ot and then permanently be locked out of using it. + + This option allows you to change specific elevators/bridges to "come to you" as well. + + - Quarry Elevator: Makes the Quarry Elevator come down when you approach it from lower Quarry and back up when you approach it from above + - Swamp Long Bridge: Rotates the side you approach it from towards you, but also rotates the other side away + - Bunker Elevator: Makes the Bunker Elevator come to any floor that you approach it from, meaning it can be accessed from the roof immediately """ - display_name = "All Bridges & Elevators come to you" + + # Used to be a toggle + @classmethod + def from_text(cls, text: str): + if text.lower() in {"off", "0", "false", "none", "null", "no"}: + raise OptionError('elevators_come_to_you is an OptionSet now. The equivalent of "false" is {}') + if text.lower() in {"on", "1", "true", "yes"}: + raise OptionError( + f'elevators_come_to_you is an OptionSet now. The equivalent of "true" is {set(cls.valid_keys)}' + ) + return super().from_text(text) + + display_name = "Elevators come to you" + + valid_keys = frozenset({"Quarry Elevator", "Swamp Long Bridge", "Bunker Elevator"}) + default = frozenset({"Quarry Elevator"}) class TrapPercentage(Range): @@ -266,6 +396,25 @@ class HintAmount(Range): default = 12 +class VagueHints(Choice): + """Make Location Hints a bit more vague, where they only tell you about the general area the item is in. + Area Hints will be generated as normal. + + If set to "stable", only location groups will be used. If location groups aren't implemented for the game your item ended up in, your hint will instead only tell you that the item is "somewhere in" that game. + If set to "experimental", region names will be eligible as well, and you will never receive a "somewhere in" hint. Keep in mind that region names are not always intended to be comprehensible to players — only turn this on if you are okay with a bit of chaos. + + + The distinction does not matter in single player, as Witness implements location groups for every location. + + Also, please don't pester any devs about implementing location groups. Bring it up nicely, accept their response even if it is "No". + """ + display_name = "Vague Hints" + + option_off = 0 + option_stable = 1 + option_experimental = 2 + + class AreaHintPercentage(Range): """ There are two types of hints for The Witness. @@ -306,6 +455,17 @@ class DeathLinkAmnesty(Range): default = 1 +class PuzzleRandomizationSeed(Range): + """ + Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization. + This option lets you set this seed yourself. + """ + display_name = "Puzzle Randomization Seed" + range_start = 1 + range_end = 9999999 + default = "random" + + @dataclass class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization @@ -318,22 +478,32 @@ class TheWitnessOptions(PerGameCommonOptions): shuffle_discarded_panels: ShuffleDiscardedPanels shuffle_vault_boxes: ShuffleVaultBoxes obelisk_keys: ObeliskKeys + unlockable_warps: UnlockableWarps shuffle_EPs: ShuffleEnvironmentalPuzzles # noqa: N815 EP_difficulty: EnvironmentalPuzzlesDifficulty shuffle_postgame: ShufflePostgame victory_condition: VictoryCondition mountain_lasers: MountainLasers challenge_lasers: ChallengeLasers + panel_hunt_total: PanelHuntTotal + panel_hunt_required_percentage: PanelHuntRequiredPercentage + panel_hunt_postgame: PanelHuntPostgame + panel_hunt_discourage_same_area_factor: PanelHuntDiscourageSameAreaFactor + panel_hunt_plando: PanelHuntPlando early_caves: EarlyCaves + early_symbol_item: EarlySymbolItem elevators_come_to_you: ElevatorsComeToYou trap_percentage: TrapPercentage trap_weights: TrapWeights puzzle_skip_amount: PuzzleSkipAmount hint_amount: HintAmount + vague_hints: VagueHints area_hint_percentage: AreaHintPercentage laser_hints: LaserHints death_link: DeathLink death_link_amnesty: DeathLinkAmnesty + puzzle_randomization_seed: PuzzleRandomizationSeed + shuffle_dog: ShuffleDog witness_option_groups = [ @@ -343,6 +513,13 @@ class TheWitnessOptions(PerGameCommonOptions): MountainLasers, ChallengeLasers, ]), + OptionGroup("Panel Hunt Options", [ + PanelHuntRequiredPercentage, + PanelHuntTotal, + PanelHuntPostgame, + PanelHuntDiscourageSameAreaFactor, + PanelHuntPlando, + ], start_collapsed=True), OptionGroup("Locations", [ ShuffleDiscardedPanels, ShuffleVaultBoxes, @@ -359,6 +536,9 @@ class TheWitnessOptions(PerGameCommonOptions): ShuffleBoat, ObeliskKeys, ]), + OptionGroup("Warps", [ + UnlockableWarps, + ]), OptionGroup("Filler Items", [ PuzzleSkipAmount, TrapPercentage, @@ -366,6 +546,7 @@ class TheWitnessOptions(PerGameCommonOptions): ]), OptionGroup("Hints", [ HintAmount, + VagueHints, AreaHintPercentage, LaserHints ]), @@ -374,5 +555,9 @@ class TheWitnessOptions(PerGameCommonOptions): ElevatorsComeToYou, DeathLink, DeathLinkAmnesty, + PuzzleRandomizationSeed, + ]), + OptionGroup("Silly Options", [ + ShuffleDog, ]) ] diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 627e5acccb90..2fb987bb456a 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -16,7 +16,7 @@ ProgressiveItemDefinition, WeightedItemDefinition, ) -from .data.utils import build_weighted_int_list +from .data.utils import build_weighted_int_list, cast_not_none from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic @@ -42,7 +42,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> None: """Adds event items after logic changes due to options""" - self._world: "WitnessWorld" = world + self._world: WitnessWorld = world self._multiworld: MultiWorld = world.multiworld self._player_id: int = world.player self._logic: WitnessPlayerLogic = player_logic @@ -54,9 +54,8 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, # Remove all progression items that aren't actually in the game. self.item_data = { name: data for (name, data) in self.item_data.items() - if data.classification not in - {ItemClassification.progression, ItemClassification.progression_skip_balancing} - or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME + if ItemClassification.progression not in data.classification + or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME } # Downgrade door items @@ -73,11 +72,11 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, # Add progression items to the mandatory item list. progression_dict = { name: data for (name, data) in self.item_data.items() - if data.classification in {ItemClassification.progression, ItemClassification.progression_skip_balancing} + if ItemClassification.progression in data.classification } for item_name, item_data in progression_dict.items(): if isinstance(item_data.definition, ProgressiveItemDefinition): - num_progression = len(self._logic.MULTI_LISTS[item_name]) + num_progression = len(self._logic.PROGRESSIVE_LISTS[item_name]) self._mandatory_items[item_name] = num_progression else: self._mandatory_items[item_name] = 1 @@ -87,7 +86,8 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, if data.classification == ItemClassification.useful}.items(): if item_name in static_witness_items._special_usefuls: continue - elif item_name == "Energy Capacity": + + if item_name == "Energy Capacity": self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES elif isinstance(item_data.classification, ProgressiveItemDefinition): self._mandatory_items[item_name] = len(item_data.mappings) @@ -96,10 +96,50 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, # Add event items to the item definition list for later lookup. for event_location in self._locations.EVENT_LOCATION_TABLE: - location_name = player_logic.EVENT_ITEM_PAIRS[event_location] + location_name = player_logic.EVENT_ITEM_PAIRS[event_location][0] self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT), ItemClassification.progression, False) + # Determine which items should be progression + useful, if they exist in some capacity. + # Note: Some of these may need to be updated for the "independent symbols" PR. + self._proguseful_items = { + "Dots", "Stars", "Shapers", "Black/White Squares", + "Caves Shortcuts", "Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)", + "Boat", + } + + if self._world.options.shuffle_EPs == "individual": + self._proguseful_items |= { + "Town Obelisk Key", # Most checks + "Monastery Obelisk Key", # Most sphere 1 checks, and also super dense ("Jackpot" vibes)} + } + + if self._world.options.shuffle_discarded_panels: + # Discards only give a moderate amount of checks, but are very spread out and a lot of them are in sphere 1. + # Thus, you really want to have the discard-unlocking item as quickly as possible. + + if self._world.options.puzzle_randomization in ("none", "sigma_normal"): + self._proguseful_items.add("Triangles") + elif self._world.options.puzzle_randomization == "sigma_expert": + self._proguseful_items.add("Arrows") + # Discards require two symbols in Variety, so the "sphere 1 unlocking power" of Arrows is not there. + if self._world.options.puzzle_randomization == "sigma_expert": + self._proguseful_items.add("Triangles") + self._proguseful_items.add("Full Dots") + self._proguseful_items.add("Stars + Same Colored Symbol") + self._proguseful_items.discard("Stars") # Stars are not that useful on their own. + if self._world.options.puzzle_randomization == "umbra_variety": + self._proguseful_items.add("Triangles") + + # This needs to be improved when the improved independent&progressive symbols PR is merged + for item in list(self._proguseful_items): + self._proguseful_items.add(static_witness_logic.get_parent_progressive_item(item)) + + for item_name, item_data in self.item_data.items(): + if item_name in self._proguseful_items: + item_data.classification |= ItemClassification.useful + + def get_mandatory_items(self) -> Dict[str, int]: """ Returns the list of items that must be in the pool for the game to successfully generate. @@ -154,16 +194,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 @@ -184,16 +220,20 @@ def get_early_items(self) -> List[str]: output -= {item for item, weight in inner_item.items() if weight} # Sort the output for consistency across versions if the implementation changes but the logic does not. - return sorted(list(output)) + return sorted(output) def get_door_ids_in_pool(self) -> List[int]: """ Returns the total set of all door IDs that are controlled by items in the pool. """ output: List[int] = [] - for item_name, item_data in {name: data for name, data in self.item_data.items() - if isinstance(data.definition, DoorItemDefinition)}.items(): - output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] + + for item_name, item_data in self.item_data.items(): + if not isinstance(item_data.definition, DoorItemDefinition): + continue + + output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes + if hex_string not in self._logic.FORBIDDEN_DOORS] return output @@ -201,18 +241,21 @@ def get_symbol_ids_not_in_pool(self) -> List[int]: """ Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool. """ - return [data.ap_code for name, data in static_witness_items.ITEM_DATA.items() - if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL] + return [ + # data.ap_code is guaranteed for a symbol definition + cast_not_none(data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() + if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL + ] def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: output: Dict[int, List[int]] = {} - for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items(): + for item_name, quantity in dict(self._mandatory_items.items()).items(): item = self.item_data[item_name] if isinstance(item.definition, ProgressiveItemDefinition): # Note: we need to reference the static table here rather than the player-specific one because the child - # items were removed from the pool when we pruned out all progression items not in the settings. - output[item.ap_code] = [static_witness_items.ITEM_DATA[child_item].ap_code - for child_item in item.definition.child_item_names] + # items were removed from the pool when we pruned out all progression items not in the options. + output[cast_not_none(item.ap_code)] = [cast_not_none(static_witness_items.ITEM_DATA[child_item].ap_code) + for child_item in item.definition.child_item_names] return output diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 05b3cf3a98e4..9e6c9597382b 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -17,11 +17,11 @@ import copy from collections import defaultdict -from logging import warning from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast from .data import static_logic as static_witness_logic from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition +from .data.static_logic import StaticWitnessLogicObj from .data.utils import ( WitnessRule, define_new_region, @@ -34,7 +34,7 @@ get_discard_exclusion_list, get_early_caves_list, get_early_caves_start_list, - get_elevators_come_to_you, + get_entity_hunt, get_ep_all_individual, get_ep_easy, get_ep_no_eclipse, @@ -50,6 +50,7 @@ logical_or_witness_rules, parse_lambda, ) +from .entity_hunt import EntityHuntPicker if TYPE_CHECKING: from . import WitnessWorld @@ -58,6 +59,107 @@ class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" + VICTORY_LOCATION: str + + def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None: + self.YAML_DISABLED_LOCATIONS: Set[str] = disabled_locations + self.YAML_ADDED_ITEMS: Dict[str, int] = start_inv + + self.EVENT_PANELS_FROM_PANELS: Set[str] = set() + self.EVENT_PANELS_FROM_REGIONS: Set[str] = set() + + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: Set[str] = set() + + self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY: Set[str] = set() + + self.UNREACHABLE_REGIONS: Set[str] = set() + + self.THEORETICAL_BASE_ITEMS: Set[str] = set() + self.THEORETICAL_ITEMS: Set[str] = set() + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + + self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = defaultdict(lambda: 1) + self.PROGRESSIVE_LISTS: Dict[str, List[str]] = {} + self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} + self.FORBIDDEN_DOORS: Set[str] = set() + + self.STARTING_INVENTORY: Set[str] = set() + + self.DIFFICULTY = world.options.puzzle_randomization + + self.REFERENCE_LOGIC: StaticWitnessLogicObj + 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 + + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME + ) + self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME + ) + self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX + ) + self.REQUIREMENTS_BY_HEX: Dict[str, WitnessRule] = {} + + self.EVENT_ITEM_PAIRS: Dict[str, Tuple[str, str]] = {} + self.COMPLETELY_DISABLED_ENTITIES: Set[str] = set() + self.DISABLE_EVERYTHING_BEHIND: Set[str] = set() + self.EXCLUDED_ENTITIES: Set[str] = set() + self.ADDED_CHECKS: Set[str] = set() + self.VICTORY_LOCATION = "0x0356B" + + self.PRE_PICKED_HUNT_ENTITIES: Set[str] = set() + self.HUNT_ENTITIES: Set[str] = set() + + self.ALWAYS_EVENT_NAMES_BY_HEX = { + "0x00509": "+1 Laser", + "0x012FB": "+1 Laser (Unredirected)", + "0x09F98": "Desert Laser Redirection", + "0xFFD03": "+1 Laser (Redirected)", + "0x01539": "+1 Laser", + "0x181B3": "+1 Laser", + "0x014BB": "+1 Laser", + "0x17C65": "+1 Laser", + "0x032F9": "+1 Laser", + "0x00274": "+1 Laser", + "0x0C2B2": "+1 Laser", + "0x00BF6": "+1 Laser", + "0x028A4": "+1 Laser", + "0x17C34": "Mountain Entry", + "0xFFF00": "Bottom Floor Discard Turns On", + } + + self.USED_EVENT_NAMES_BY_HEX: Dict[str, List[str]] = {} + self.CONDITIONAL_EVENTS: Dict[Tuple[str, str], str] = {} + + # The basic requirements to solve each entity come from StaticWitnessLogic. + # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements. + self.make_options_adjustments(world) + self.determine_unrequired_entities(world) + self.find_unsolvable_entities(world) + + # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements. + # This will make the access conditions way faster, instead of recursively checking dependent entities each time. + self.make_dependency_reduced_checklist() + + if world.options.victory_condition == "panel_hunt": + picker = EntityHuntPicker(self, world, self.PRE_PICKED_HUNT_ENTITIES) + self.HUNT_ENTITIES = picker.pick_panel_hunt_panels(world.options.panel_hunt_total.value) + + # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. + self.finalize_items() + + # Create event-item pairs for specific panels in the game. + self.make_event_panel_lists() + def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: """ Panels in this game often only turn on when other panels are solved. @@ -77,64 +179,67 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # For the requirement of an entity, we consider two things: # 1. Any items this entity needs (e.g. Symbols or Door Items) - these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()})) + these_items: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()})) # 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set) - these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"] + these_panels: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"] # Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off) these_items = frozenset({ - subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI) + subset.intersection(self.THEORETICAL_BASE_ITEMS) for subset in these_items }) # Update the list of "items that are actually being used by any entity" for subset in these_items: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.update(subset) # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. # Also, remove any original power requirements this entity might have had. - if entity_hex in self.DOOR_ITEMS_BY_ID: + if entity_hex in self.DOOR_ITEMS_BY_ID and entity_hex not in self.FORBIDDEN_DOORS: + # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]}) for dependent_item in door_items: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item) + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.update(dependent_item) - all_options = logical_and_witness_rules([door_items, these_items]) + these_items = logical_and_witness_rules([door_items, these_items]) - # If this entity is not an EP, and it has an associated door item, ignore the original power dependencies - if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP": + # A door entity is opened by its door item instead of previous entities powering it. + # That means we need to ignore any dependent requirements. + # However, there are some entities that depend on other entities because of an environmental reason. + # Those requirements need to be preserved even in door shuffle. + entity_dependencies_need_to_be_preserved = ( + # EPs keep all their entity dependencies + static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "EP" # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved, # except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. # In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies. - if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels): - these_items = all_options - + or entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels) # Another dependency that is not power-based: The Symmetry Island Upper Panel latches - elif entity_hex == "0x1C349": - these_items = all_options - - else: - return frozenset(all_options) + or entity_hex == "0x1C349" + ) - else: - these_items = all_options + # If this is not one of those special cases, solving this door entity only needs its own item requirement. + # Dependent entities from these_panels are ignored, and we just return these_items directly. + if not entity_dependencies_need_to_be_preserved: + return these_items # Now that we have item requirements and entity dependencies, it's time for the dependency reduction. # For each entity that this entity depends on (e.g. a panel turning on another panel), # Add that entities requirements to this entity. # If there are multiple options, consider each, and then or-chain them. - all_options = list() + all_options = [] for option in these_panels: - dependent_items_for_option = frozenset({frozenset()}) + dependent_items_for_option: WitnessRule = frozenset({frozenset()}) # For each entity in this option, resolve it to its actual requirement. for option_entity in option: - dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) + dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity, {}) if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", - "PP2 Weirdness", "Theater to Tunnels"}: + "PP2 Weirdness", "Theater to Tunnels", "Entity Hunt"}: new_items = frozenset({frozenset([option_entity])}) elif option_entity in self.DISABLE_EVERYTHING_BEHIND: new_items = frozenset() @@ -149,12 +254,12 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # If the dependent entity is unsolvable and is NOT an EP, this requirement option is invalid. new_items = frozenset() elif option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX: - new_items = frozenset({frozenset([option_entity])}) + new_items = frozenset({frozenset([self.ALWAYS_EVENT_NAMES_BY_HEX[option_entity]])}) elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS: - new_items = frozenset({frozenset([option_entity])}) - self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[ - (entity_hex, option_entity) - ] + new_items = frozenset({frozenset([self.CONDITIONAL_EVENTS[(entity_hex, option_entity)]])}) + self.USED_EVENT_NAMES_BY_HEX[option_entity].append( + self.CONDITIONAL_EVENTS[(entity_hex, option_entity)] + ) else: new_items = theoretical_new_items if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]: @@ -197,10 +302,10 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: self.THEORETICAL_ITEMS.add(item_name) if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition, - static_witness_logic.ALL_ITEMS[item_name]).child_item_names) + self.THEORETICAL_BASE_ITEMS.update(cast(ProgressiveItemDefinition, + static_witness_logic.ALL_ITEMS[item_name]).child_item_names) else: - self.THEORETICAL_ITEMS_NO_MULTI.add(item_name) + self.THEORETICAL_BASE_ITEMS.add(item_name) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -214,11 +319,11 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: self.THEORETICAL_ITEMS.discard(item_name) if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition): - self.THEORETICAL_ITEMS_NO_MULTI.difference_update( + self.THEORETICAL_BASE_ITEMS.difference_update( cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names ) else: - self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name) + self.THEORETICAL_BASE_ITEMS.discard(item_name) if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes @@ -226,6 +331,10 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: if entity_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[entity_hex]: self.DOOR_ITEMS_BY_ID[entity_hex].remove(item_name) + if adj_type == "Forbidden Doors": + entity_hex = line[:7] + self.FORBIDDEN_DOORS.add(entity_hex) + if adj_type == "Starting Inventory": self.STARTING_INVENTORY.add(line) @@ -312,7 +421,7 @@ def make_single_adjustment(self, adj_type: str, line: str) -> None: line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"] self.ADDED_CHECKS.add(line) - def handle_postgame(self, world: "WitnessWorld") -> List[List[str]]: + def handle_regular_postgame(self, world: "WitnessWorld") -> List[List[str]]: """ In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled. This mostly involves the disabling of key panels (e.g. long box when the goal is short box). @@ -343,6 +452,7 @@ def handle_postgame(self, world: "WitnessWorld") -> List[List[str]]: # If we have a long box goal, Challenge is behind the amount of lasers required to just win. # This is technically slightly incorrect as the Challenge Vault Box could contain a *symbol* that is required # to open Mountain Entry (Stars 2). However, since there is a very easy sphere 1 snipe, this is not considered. + if victory == "mountain_box_long": postgame_adjustments.append(["Disabled Locations:", "0x0A332 (Challenge Timer Start)"]) @@ -387,6 +497,42 @@ def handle_postgame(self, world: "WitnessWorld") -> List[List[str]]: return postgame_adjustments + def handle_panelhunt_postgame(self, world: "WitnessWorld") -> List[List[str]]: + postgame_adjustments = [] + + # Make some quick references to some options + panel_hunt_postgame = world.options.panel_hunt_postgame + chal_lasers = world.options.challenge_lasers + + disable_mountain_lasers = ( + panel_hunt_postgame == "disable_mountain_lasers_locations" + or panel_hunt_postgame == "disable_anything_locked_by_lasers" + ) + + disable_challenge_lasers = ( + panel_hunt_postgame == "disable_challenge_lasers_locations" + or panel_hunt_postgame == "disable_anything_locked_by_lasers" + ) + + if disable_mountain_lasers: + self.DISABLE_EVERYTHING_BEHIND.add("0x09F7F") # Short box + self.PRE_PICKED_HUNT_ENTITIES.add("0x09F7F") + self.COMPLETELY_DISABLED_ENTITIES.add("0x3D9A9") # Elevator Start + + # If mountain lasers are disabled, and challenge lasers > 7, the box will need to be rotated + if chal_lasers > 7: + postgame_adjustments.append([ + "Requirement Changes:", + "0xFFF00 - 11 Lasers - True", + ]) + + if disable_challenge_lasers: + self.DISABLE_EVERYTHING_BEHIND.add("0xFFF00") # Long box + self.PRE_PICKED_HUNT_ENTITIES.add("0xFFF00") + self.COMPLETELY_DISABLED_ENTITIES.add("0x0A332") # Challenge Timer + + return postgame_adjustments + def make_options_adjustments(self, world: "WitnessWorld") -> None: """Makes logic adjustments based on options""" adjustment_linesets_in_order = [] @@ -408,10 +554,17 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: self.VICTORY_LOCATION = "0x09F7F" elif victory == "mountain_box_long": self.VICTORY_LOCATION = "0xFFF00" + elif victory == "panel_hunt": + self.VICTORY_LOCATION = "0x03629" + self.COMPLETELY_DISABLED_ENTITIES.add("0x3352F") + + # Exclude panels from the post-game if shuffle_postgame is false. + if not world.options.shuffle_postgame and victory != "panel_hunt": + adjustment_linesets_in_order += self.handle_regular_postgame(world) # Exclude panels from the post-game if shuffle_postgame is false. - if not world.options.shuffle_postgame: - adjustment_linesets_in_order += self.handle_postgame(world) + if victory == "panel_hunt" and world.options.panel_hunt_postgame: + adjustment_linesets_in_order += self.handle_panelhunt_postgame(world) # Exclude Discards / Vaults if not world.options.shuffle_discarded_panels: @@ -466,6 +619,9 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: adjustment_linesets_in_order.append(get_complex_doors()) adjustment_linesets_in_order.append(get_complex_additional_panels()) + if not world.options.shuffle_dog: + adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF80 (Town Pet the Dog)"]) + if world.options.shuffle_boat: adjustment_linesets_in_order.append(get_boat()) @@ -475,8 +631,32 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: if world.options.early_caves == "add_to_pool" and not remote_doors: adjustment_linesets_in_order.append(get_early_caves_list()) - if world.options.elevators_come_to_you: - adjustment_linesets_in_order.append(get_elevators_come_to_you()) + if "Quarry Elevator" in world.options.elevators_come_to_you: + adjustment_linesets_in_order.append([ + "New Connections:", + "Quarry - Quarry Elevator - TrueOneWay", + "Outside Quarry - Quarry Elevator - TrueOneWay", + ]) + if "Bunker Elevator" in world.options.elevators_come_to_you: + adjustment_linesets_in_order.append([ + "New Connections:", + "Outside Bunker - Bunker Elevator - TrueOneWay", + ]) + if "Swamp Long Bridge" in world.options.elevators_come_to_you: + adjustment_linesets_in_order.append([ + "New Connections:", + "Outside Swamp - Swamp Long Bridge - TrueOneWay", + "Swamp Near Boat - Swamp Long Bridge - TrueOneWay", + "Requirement Changes:", + "0x035DE - 0x17E2B - True", # Swamp Purple Sand Bottom EP + ]) + # if "Town Maze Rooftop Bridge" in world.options.elevators_come_to_you: + # adjustment_linesets_in_order.append([ + # "New Connections:" + # "Town Red Rooftop - Town Maze Rooftop - TrueOneWay" + + if world.options.victory_condition == "panel_hunt": + adjustment_linesets_in_order.append(get_entity_hunt()) for item in self.YAML_ADDED_ITEMS: adjustment_linesets_in_order.append(["Items:", item]) @@ -511,8 +691,8 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: if loc_obj["entityType"] == "EP": self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"]) - elif loc_obj["entityType"] in {"General", "Vault", "Discard"}: - self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"]) + elif loc_obj["entityType"] == "Panel": + self.EXCLUDED_ENTITIES.add(loc_obj["entity_hex"]) for adjustment_lineset in adjustment_linesets_in_order: current_adjustment_type = None @@ -525,13 +705,16 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: current_adjustment_type = line[:-1] continue + if current_adjustment_type is None: + raise ValueError(f"Adjustment lineset {adjustment_lineset} is malformed") + self.make_single_adjustment(current_adjustment_type, line) - for entity_id in self.COMPLETELY_DISABLED_ENTITIES: + for entity_id in self.COMPLETELY_DISABLED_ENTITIES | self.FORBIDDEN_DOORS: if entity_id in self.DOOR_ITEMS_BY_ID: del self.DOOR_ITEMS_BY_ID[entity_id] - def discover_reachable_regions(self): + def discover_reachable_regions(self) -> Set[str]: """ Some options disable panels or remove specific items. This can make entire regions completely unreachable, because all their incoming connections are invalid. @@ -591,6 +774,7 @@ def find_unsolvable_entities(self, world: "WitnessWorld") -> None: # Check if any regions have become unreachable. reachable_regions = self.discover_reachable_regions() new_unreachable_regions = all_regions - reachable_regions - self.UNREACHABLE_REGIONS + if new_unreachable_regions: self.UNREACHABLE_REGIONS.update(new_unreachable_regions) @@ -621,8 +805,7 @@ def find_unsolvable_entities(self, world: "WitnessWorld") -> None: # If we are disabling a laser, something has gone wrong. if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser": laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"] - player_name = world.multiworld.get_player_name(world.player) - raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}." + raise RuntimeError(f"Somehow, {laser_name} was disabled for player {world.player_name}." f" This is not allowed to happen, please report to Violet.") newly_discovered_disabled_entities.add(entity) @@ -640,15 +823,18 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> # Check each traversal option individually for option in connection[1]: - individual_entity_requirements = [] + individual_entity_requirements: List[WitnessRule] = [] for entity in option: # If a connection requires solving a disabled entity, it is not valid. if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND: individual_entity_requirements.append(frozenset()) # If a connection requires acquiring an event, add that event to its requirements. - elif (entity in self.ALWAYS_EVENT_NAMES_BY_HEX - or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX): + elif entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX: individual_entity_requirements.append(frozenset({frozenset({entity})})) + elif entity in self.ALWAYS_EVENT_NAMES_BY_HEX: + individual_entity_requirements.append( + frozenset({frozenset({self.ALWAYS_EVENT_NAMES_BY_HEX[entity]})}) + ) # If a connection requires entities, use their newly calculated independent requirements. else: entity_req = self.get_entity_requirement(entity) @@ -664,7 +850,7 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> return logical_or_witness_rules(all_possibilities) - def make_dependency_reduced_checklist(self): + def make_dependency_reduced_checklist(self) -> None: """ Every entity has a requirement. This requirement may involve other entities. Example: Solving a panel powers a cable, and that cable turns on the next panel. @@ -679,13 +865,13 @@ def make_dependency_reduced_checklist(self): # Requirements are cached per entity. However, we might redo the whole reduction process multiple times. # So, we first clear this cache. - self.REQUIREMENTS_BY_HEX = dict() + self.REQUIREMENTS_BY_HEX = {} # We also clear any data structures that we might have filled in a previous dependency reduction - self.REQUIREMENTS_BY_HEX = dict() - self.USED_EVENT_NAMES_BY_HEX = dict() - self.CONNECTIONS_BY_REGION_NAME = dict() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() + self.REQUIREMENTS_BY_HEX = {} + self.USED_EVENT_NAMES_BY_HEX = defaultdict(list) + self.CONNECTIONS_BY_REGION_NAME = {} + self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME = set() # Make independent requirements for entities for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys(): @@ -695,37 +881,33 @@ def make_dependency_reduced_checklist(self): # Make independent region connection requirements based on the entities they require for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items(): - self.CONNECTIONS_BY_REGION_NAME[region] = [] - - new_connections = [] + new_connections = set() for connection in connections: overall_requirement = self.reduce_connection_requirement(connection) # If there is a way to use this connection, add it. if overall_requirement: - new_connections.append((connection[0], overall_requirement)) + new_connections.add((connection[0], overall_requirement)) - # If there are any usable outgoing connections from this region, add them. - if new_connections: - self.CONNECTIONS_BY_REGION_NAME[region] = new_connections + self.CONNECTIONS_BY_REGION_NAME[region] = new_connections - def finalize_items(self): + def finalize_items(self) -> None: """ Finalise which items are used in the world, and handle their progressive versions. """ - for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: + for item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: if item not in self.THEORETICAL_ITEMS: progressive_item_name = static_witness_logic.get_parent_progressive_item(item) - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name) + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name) child_items = cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names - multi_list = [child_item for child_item in child_items - if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI] - self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1 - self.MULTI_LISTS[progressive_item_name] = multi_list + progressive_list = [child_item for child_item in child_items + if child_item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME] + self.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] = progressive_list.index(item) + 1 + self.PROGRESSIVE_LISTS[progressive_item_name] = progressive_list else: - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) + self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(item) def solvability_guaranteed(self, entity_hex: str) -> bool: return not ( @@ -741,7 +923,7 @@ def is_disabled(self, entity_hex: str) -> bool: ) def determine_unrequired_entities(self, world: "WitnessWorld") -> None: - """Figure out which major items are actually useless in this world's settings""" + """Figure out which major items are actually useless in this world's options""" # Gather quick references to relevant options eps_shuffled = world.options.shuffle_EPs @@ -777,7 +959,6 @@ def determine_unrequired_entities(self, world: "WitnessWorld") -> None: "0x17CC4": come_to_you or eps_shuffled, # Quarry Elevator Panel "0x17E2B": come_to_you and boat_shuffled or eps_shuffled, # Swamp Long Bridge "0x0CF2A": False, # Jungle Monastery Garden Shortcut - "0x17CAA": remote_doors, # Jungle Monastery Garden Shortcut Panel "0x0364E": False, # Monastery Laser Shortcut Door "0x03713": remote_doors, # Monastery Laser Shortcut Panel "0x03313": False, # Orchard Second Gate @@ -793,120 +974,54 @@ def determine_unrequired_entities(self, world: "WitnessWorld") -> None: # Jungle Popup Wall Panel } + # In panel hunt, all panels are game, so all panels need to be reachable (unless disabled) + if goal == "panel_hunt": + for entity_hex in is_item_required_dict: + if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Panel": + is_item_required_dict[entity_hex] = True + # Now, return the keys of the dict entries where the result is False to get unrequired major items self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY |= { item_name for item_name, is_required in is_item_required_dict.items() if not is_required } - def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]: - """ - Makes a pair of an event panel and its event item - """ - action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved" - - name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]["checkName"] + action - if entity_hex not in self.USED_EVENT_NAMES_BY_HEX: - warning(f'Entity "{name}" does not have an associated event name.') - self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event" - pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex]) - return pair - def make_event_panel_lists(self) -> None: """ Makes event-item pairs for entities with associated events, unless these entities are disabled. """ - self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" + self.USED_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION].append("Victory") - self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX) + for event_hex, event_name in self.ALWAYS_EVENT_NAMES_BY_HEX.items(): + self.USED_EVENT_NAMES_BY_HEX[event_hex].append(event_name) self.USED_EVENT_NAMES_BY_HEX = { - event_hex: event_name for event_hex, event_name in self.USED_EVENT_NAMES_BY_HEX.items() + event_hex: event_list for event_hex, event_list in self.USED_EVENT_NAMES_BY_HEX.items() if self.solvability_guaranteed(event_hex) } - for panel in self.USED_EVENT_NAMES_BY_HEX: - pair = self.make_event_item_pair(panel) - self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] - - def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None: - self.YAML_DISABLED_LOCATIONS = disabled_locations - self.YAML_ADDED_ITEMS = start_inv - - self.EVENT_PANELS_FROM_PANELS = set() - self.EVENT_PANELS_FROM_REGIONS = set() - - self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() - - self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set() - - self.UNREACHABLE_REGIONS = set() - - self.THEORETICAL_ITEMS = set() - self.THEORETICAL_ITEMS_NO_MULTI = set() - self.MULTI_AMOUNTS = defaultdict(lambda: 1) - self.MULTI_LISTS = dict() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() - self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} - self.STARTING_INVENTORY = set() + for entity_hex, event_names in self.USED_EVENT_NAMES_BY_HEX.items(): + entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex] + entity_name = entity_obj["checkName"] + entity_type = entity_obj["entityType"] - self.DIFFICULTY = world.options.puzzle_randomization - - 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 == "none": - self.REFERENCE_LOGIC = static_witness_logic.vanilla - - self.CONNECTIONS_BY_REGION_NAME_THEORETICAL = copy.deepcopy( - self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME - ) - self.CONNECTIONS_BY_REGION_NAME = dict() - self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) - self.REQUIREMENTS_BY_HEX = dict() - - self.EVENT_ITEM_PAIRS = dict() - self.COMPLETELY_DISABLED_ENTITIES = set() - self.DISABLE_EVERYTHING_BEHIND = set() - self.PRECOMPLETED_LOCATIONS = set() - self.EXCLUDED_LOCATIONS = set() - self.ADDED_CHECKS = set() - self.VICTORY_LOCATION: str - - self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x00509": "+1 Laser (Symmetry Laser)", - "0x012FB": "+1 Laser (Desert Laser)", - "0x09F98": "Desert Laser Redirection", - "0x01539": "+1 Laser (Quarry Laser)", - "0x181B3": "+1 Laser (Shadows Laser)", - "0x014BB": "+1 Laser (Keep Laser)", - "0x17C65": "+1 Laser (Monastery Laser)", - "0x032F9": "+1 Laser (Town Laser)", - "0x00274": "+1 Laser (Jungle Laser)", - "0x0C2B2": "+1 Laser (Bunker Laser)", - "0x00BF6": "+1 Laser (Swamp Laser)", - "0x028A4": "+1 Laser (Treehouse Laser)", - "0x17C34": "Mountain Entry", - "0xFFF00": "Bottom Floor Discard Turns On", - } - - self.USED_EVENT_NAMES_BY_HEX = {} - self.CONDITIONAL_EVENTS = {} - - # The basic requirements to solve each entity come from StaticWitnessLogic. - # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements. - self.make_options_adjustments(world) - self.determine_unrequired_entities(world) - self.find_unsolvable_entities(world) + if entity_type == "Door": + action = " Opened" + elif entity_type == "Laser": + action = " Activated" + else: + action = " Solved" - # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements. - # This will make the access conditions way faster, instead of recursively checking dependent entities each time. - self.make_dependency_reduced_checklist() + for i, event_name in enumerate(event_names): + if i == 0: + self.EVENT_ITEM_PAIRS[entity_name + action] = (event_name, entity_hex) + else: + self.EVENT_ITEM_PAIRS[entity_name + action + f" (Effect {i + 1})"] = (event_name, entity_hex) - # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. - self.finalize_items() + # Make Panel Hunt Events + for entity_hex in self.HUNT_ENTITIES: + entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex] + entity_name = entity_obj["checkName"] + self.EVENT_ITEM_PAIRS[entity_name + " (Panel Hunt)"] = ("+1 Panel Hunt", entity_hex) - # Create event-item pairs for specific panels in the game. - self.make_event_panel_lists() + return diff --git a/worlds/witness/presets.py b/worlds/witness/presets.py index 2a53484a4c77..687d74f771cb 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, @@ -28,7 +35,8 @@ "challenge_lasers": 11, "early_caves": EarlyCaves.option_off, - "elevators_come_to_you": False, + + "elevators_come_to_you": ElevatorsComeToYou.default, "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": PuzzleSkipAmount.default, @@ -37,6 +45,8 @@ "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.default, }, # For relative beginners who want to move to the next step. @@ -64,7 +74,8 @@ "challenge_lasers": 9, "early_caves": EarlyCaves.option_off, - "elevators_come_to_you": False, + + "elevators_come_to_you": ElevatorsComeToYou.default, "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": 15, @@ -73,6 +84,8 @@ "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.default, }, # Allsanity but without the BS (no expert, no tedious EPs). @@ -100,7 +113,8 @@ "challenge_lasers": 9, "early_caves": EarlyCaves.option_off, - "elevators_come_to_you": True, + + "elevators_come_to_you": ElevatorsComeToYou.valid_keys, "trap_percentage": TrapPercentage.default, "puzzle_skip_amount": 15, @@ -109,5 +123,7 @@ "laser_hints": LaserHints.default, "death_link": DeathLink.default, "death_link_amnesty": DeathLinkAmnesty.default, + + "shuffle_dog": ShuffleDog.option_random_item, }, } diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 35f4e9544212..1df438f68b0d 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -3,15 +3,16 @@ and connects them with the proper requirements """ from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Set, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from BaseClasses import Entrance, Region from worlds.generic.Rules import CollectionRule from .data import static_logic as static_witness_logic +from .data.static_logic import StaticWitnessLogicObj from .data.utils import WitnessRule, optimize_witness_rule -from .locations import WitnessPlayerLocations, static_witness_locations +from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic if TYPE_CHECKING: @@ -21,11 +22,25 @@ class WitnessPlayerRegions: """Class that defines Witness Regions""" - player_locations = None - logic = None + def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: + difficulty = world.options.puzzle_randomization + + self.reference_logic: StaticWitnessLogicObj + if difficulty == "sigma_normal": + 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 + + self.player_locations = player_locations + self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) + self.created_region_names: Set[str] = set() @staticmethod - def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule: + def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]: from .rules import _meets_item_requirements """ @@ -36,7 +51,7 @@ def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Collect return _meets_item_requirements(item_requirement, world) def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule, - regions_by_name: Dict[str, Region]): + regions_by_name: Dict[str, Region]) -> None: """ connect two regions and set the corresponding requirement """ @@ -66,7 +81,9 @@ def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, r source_region ) - connection.access_rule = self.make_lambda(final_requirement, world) + rule = self.make_lambda(final_requirement, world) + if rule is not None: + connection.access_rule = rule source_region.exits.append(connection) connection.connect(target_region) @@ -89,24 +106,32 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic """ from . import create_region - all_locations = set() - regions_by_name = dict() + all_locations: Set[str] = set() + regions_by_name: Dict[str, Region] = {} regions_to_create = { k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items() if k not in player_logic.UNREACHABLE_REGIONS } + event_locations_per_region = defaultdict(list) + + for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items(): + region = static_witness_logic.ENTITIES_BY_HEX[event_item_and_entity[1]]["region"] + if region is None: + region_name = "Entry" + else: + region_name = region["name"] + event_locations_per_region[region_name].append(event_location) + for region_name, region in regions_to_create.items(): locations_for_this_region = [ self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["entities"] if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] in self.player_locations.CHECK_LOCATION_TABLE ] - locations_for_this_region += [ - static_witness_locations.get_event_name(panel) for panel in region["entities"] - if static_witness_locations.get_event_name(panel) in self.player_locations.EVENT_LOCATION_TABLE - ] + + locations_for_this_region += event_locations_per_region[region_name] all_locations = all_locations | set(locations_for_this_region) @@ -121,17 +146,3 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic for region_name, region in regions_to_create.items(): for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name) - - def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: - difficulty = world.options.puzzle_randomization - - if difficulty == "sigma_normal": - self.reference_logic = static_witness_logic.sigma_normal - elif difficulty == "sigma_expert": - self.reference_logic = static_witness_logic.sigma_expert - elif difficulty == "none": - self.reference_logic = static_witness_logic.vanilla - - self.player_locations = player_locations - self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) - self.created_region_names: Set[str] = set() diff --git a/worlds/witness/ruff.toml b/worlds/witness/ruff.toml index d42361a4aaa9..a35711cce66d 100644 --- a/worlds/witness/ruff.toml +++ b/worlds/witness/ruff.toml @@ -1,10 +1,10 @@ line-length = 120 [lint] -select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] -ignore = ["RUF012", "RUF100"] +select = ["C", "E", "F", "R", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] +ignore = ["C9", "RUF012", "RUF100"] -[per-file-ignores] +[lint.per-file-ignores] # The way options definitions work right now, I am forced to break line length requirements. "options.py" = ["E501"] # The import list would just be so big if I imported every option individually in presets.py diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index b4982d1830b2..dac1556e46d4 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -2,7 +2,8 @@ Defines the rules by which locations can be accessed, depending on the items received """ -from typing import TYPE_CHECKING +from collections import Counter +from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union from BaseClasses import CollectionState @@ -10,61 +11,27 @@ from .data import static_logic as static_witness_logic from .data.utils import WitnessRule -from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic if TYPE_CHECKING: from . import WitnessWorld -laser_hexes = [ - "0x028A4", - "0x00274", - "0x032F9", - "0x01539", - "0x181B3", - "0x0C2B2", - "0x00509", - "0x00BF6", - "0x014BB", - "0x012FB", - "0x17C65", -] - - -def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_required: bool) -> CollectionRule: - if laser_hex == "0x012FB" and redirect_required: - return lambda state: ( - _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state) - and state.has("Desert Laser Redirection", player) - ) - else: - return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations) - -def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: - laser_lambdas = [] - - for laser_hex in laser_hexes: - has_laser_lambda = _has_laser(laser_hex, world, world.player, redirect_required) +class SimpleItemRepresentation(NamedTuple): + item_name: str + item_count: int - laser_lambdas.append(has_laser_lambda) - return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount +def _can_do_panel_hunt(world: "WitnessWorld") -> SimpleItemRepresentation: + required = world.panel_hunt_required_count + return SimpleItemRepresentation("+1 Panel Hunt", required) -def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic, - player_locations: WitnessPlayerLocations) -> CollectionRule: - """ - Determines whether a panel can be solved - """ +def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: + if redirect_required: + return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Redirected)"], world.player, amount) - panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel] - entity_name = panel_obj["checkName"] - - if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE: - return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player) - else: - return make_lambda(panel, world) + return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Unredirected)"], world.player, amount) def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: @@ -74,10 +41,10 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: """ player = world.player - player_regions = world.player_regions + two_way_entrance_register = world.player_regions.two_way_entrance_register front_access = ( - any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Pressure Plate", "Keep"]) + any(e.can_reach(state) for e in two_way_entrance_register["Keep 2nd Pressure Plate", "Keep"]) and state.can_reach_region("Keep", player) ) @@ -88,7 +55,7 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # Front access works. Now, we need to check for the many ways to access PP2 from the back. # All of those ways lead through the PP3 exit door from PP4. So we check this first. - fourth_to_third = any(e.can_reach(state) for e in player_regions.two_way_entrance_register[ + fourth_to_third = any(e.can_reach(state) for e in two_way_entrance_register[ "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate" ]) @@ -100,7 +67,7 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # The shadows shortcut is the simplest way. shadows_shortcut = ( - any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Shadows"]) + any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Pressure Plate", "Shadows"]) ) if shadows_shortcut: @@ -108,9 +75,7 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # We don't have the Shadows shortcut. This means we need to come in through the PP4 exit door instead. - tower_to_pp4 = any( - e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Pressure Plate", "Keep Tower"] - ) + tower_to_pp4 = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Pressure Plate", "Keep Tower"]) # If we don't have the PP4 exit door, we've run out of options. if not tower_to_pp4: @@ -119,7 +84,7 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # We have the PP4 exit door. If we can get to Keep Tower from behind, we can do PP2. # The simplest way would be the Tower Shortcut. - tower_shortcut = any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep", "Keep Tower"]) + tower_shortcut = any(e.can_reach(state) for e in two_way_entrance_register["Keep", "Keep Tower"]) if tower_shortcut: return True @@ -128,18 +93,14 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # Getting to Keep Tower through the hedge mazes. This can be done in a multitude of ways. # No matter what, though, we would need Hedge Maze 4 Exit to Keep Tower. - tower_access_from_hedges = any( - e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep Tower"] - ) + tower_access_from_hedges = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Maze", "Keep Tower"]) if not tower_access_from_hedges: return False # We can reach Keep Tower from Hedge Maze 4. If we now have the Hedge 4 Shortcut, we are immediately good. - hedge_4_shortcut = any( - e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep"] - ) + hedge_4_shortcut = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Maze", "Keep"]) # If we have the hedge 4 shortcut, that works. if hedge_4_shortcut: @@ -147,27 +108,21 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # We don't have the hedge 4 shortcut. This means we would now need to come through Hedge Maze 3. - hedge_3_to_4 = any( - e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 4th Maze", "Keep 3rd Maze"] - ) + hedge_3_to_4 = any(e.can_reach(state) for e in two_way_entrance_register["Keep 4th Maze", "Keep 3rd Maze"]) if not hedge_3_to_4: return False # We can get to Hedge 4 from Hedge 3. If we have the Hedge 3 Shortcut, we're good. - hedge_3_shortcut = any( - e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep"] - ) + hedge_3_shortcut = any(e.can_reach(state) for e in two_way_entrance_register["Keep 3rd Maze", "Keep"]) if hedge_3_shortcut: return True # We don't have Hedge 3 Shortcut. This means we would now need to come through Hedge Maze 2. - hedge_2_to_3 = any( - e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 3rd Maze", "Keep 2nd Maze"] - ) + hedge_2_to_3 = any(e.can_reach(state) for e in two_way_entrance_register["Keep 3rd Maze", "Keep 2nd Maze"]) if not hedge_2_to_3: return False @@ -175,11 +130,7 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # We can get to Hedge 3 from Hedge 2. If we can get from Keep to Hedge 2, we're good. # This covers both Hedge 1 Exit and Hedge 2 Shortcut, because Hedge 1 is just part of the Keep region. - hedge_2_from_keep = any( - e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Maze", "Keep"] - ) - - return hedge_2_from_keep + return any(e.can_reach(state) for e in two_way_entrance_register["Keep 2nd Maze", "Keep"]) def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool: @@ -191,11 +142,11 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> # Checking for access to Theater is not necessary, as solvability of Tutorial Video is checked in the other half # of the Theater Flowers EP condition. - player_regions = world.player_regions + two_way_entrance_register = world.player_regions.two_way_entrance_register direct_access = ( - any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"]) - and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Theater", "Windmill Interior"]) + any(e.can_reach(state) for e in two_way_entrance_register["Tunnels", "Windmill Interior"]) + and any(e.can_reach(state) for e in two_way_entrance_register["Theater", "Windmill Interior"]) ) if direct_access: @@ -211,17 +162,22 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> # We also need a way from Town to Tunnels. - tunnels_from_town = ( - any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"]) - and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Town", "Windmill Interior"]) - or any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Town"]) + return ( + any(e.can_reach(state) for e in two_way_entrance_register["Tunnels", "Windmill Interior"]) + and any(e.can_reach(state) for e in two_way_entrance_register["Outside Windmill", "Windmill Interior"]) + or any(e.can_reach(state) for e in two_way_entrance_register["Tunnels", "Town"]) ) - return tunnels_from_town +def _has_item(item: str, world: "WitnessWorld", + player_logic: WitnessPlayerLogic) -> Union[CollectionRule, SimpleItemRepresentation]: + """ + Convert a single element of a WitnessRule into a CollectionRule, unless it is referring to an item, + in which case we return it as an item-count pair ("SimpleItemRepresentation"). This allows some optimisation later. + """ + + assert item not in static_witness_logic.ENTITIES_BY_HEX, "Requirements can no longer contain entity hexes directly." -def _has_item(item: str, world: "WitnessWorld", player: int, - player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule: if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME: region = world.get_region(item) return region.can_reach @@ -237,35 +193,108 @@ def _has_item(item: str, world: "WitnessWorld", player: int, if item == "11 Lasers + Redirect": laser_req = world.options.challenge_lasers.value return _has_lasers(laser_req, world, True) - elif item == "PP2 Weirdness": + if item == "Entity Hunt": + # Right now, panel hunt is the only type of entity hunt. This may need to be changed later + return _can_do_panel_hunt(world) + if item == "PP2 Weirdness": return lambda state: _can_do_expert_pp2(state, world) - elif item == "Theater to Tunnels": + if item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) - if item in player_logic.USED_EVENT_NAMES_BY_HEX: - return _can_solve_panel(item, world, player, player_logic, player_locations) - prog_item = static_witness_logic.get_parent_progressive_item(item) - return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]) + actual_item = static_witness_logic.get_parent_progressive_item(item) + needed_amount = player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] + + simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(actual_item, needed_amount) + return simple_rule -def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule: +def optimize_requirement_option(requirement_option: List[Union[CollectionRule, SimpleItemRepresentation]])\ + -> List[Union[CollectionRule, SimpleItemRepresentation]]: """ - Checks whether item and panel requirements are met for - a panel + This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version. """ - lambda_conversion = [ - [_has_item(item, world, world.player, world.player_logic, world.player_locations) for item in subset] + direct_items = [rule for rule in requirement_option if isinstance(rule, SimpleItemRepresentation)] + if not direct_items: + return requirement_option + + max_per_item: Dict[str, int] = Counter() + for item_rule in direct_items: + max_per_item[item_rule[0]] = max(max_per_item[item_rule[0]], item_rule[1]) + + return [ + rule for rule in requirement_option + if not (isinstance(rule, SimpleItemRepresentation) and rule[1] < max_per_item[rule[0]]) + ] + + +def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]], + player: int) -> List[CollectionRule]: + """ + 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() + """ + + collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)] + item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)] + + if len(item_rules) == 0: + item_rules_converted = [] + elif len(item_rules) == 1: + item = item_rules[0][0] + count = item_rules[0][1] + item_rules_converted = [lambda state: state.has(item, player, count)] + else: + item_counts = {item_rule.item_name: item_rule.item_count for item_rule in item_rules} + # Sort the list by which item you are least likely to have (E.g. last stage of progressive item chains) + sorted_item_list = sorted( + item_counts.keys(), + key=lambda item_name: item_counts[item_name] if ("Progressive" in item_name) else 1.5, + reverse=True + # 1.5 because you are less likely to have a single stage item than one copy of a 2-stage chain + # I did some testing and every part of this genuinely gives a tiiiiny performance boost over not having it! + ) + + if all(item_count == 1 for item_count in item_counts.values()): + # If all counts are one, just use state.has_all + item_rules_converted = [lambda state: state.has_all(sorted_item_list, player)] + else: + # If any count is higher than 1, use state.has_all_counts + sorted_item_counts = {item_name: item_counts[item_name] for item_name in sorted_item_list} + item_rules_converted = [lambda state: state.has_all_counts(sorted_item_counts, player)] + + return collection_rules + item_rules_converted + + +def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]: + """ + Converts a WitnessRule into a CollectionRule. + """ + player = world.player + + if requirements == frozenset({frozenset()}): + return None + + rule_conversion = [ + [_has_item(item, world, world.player_logic) for item in subset] for subset in requirements ] + optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion] + + fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion] + + if len(fully_converted_rules) == 1: + if len(fully_converted_rules[0]) == 1: + return fully_converted_rules[0][0] + return lambda state: all(condition(state) for condition in fully_converted_rules[0]) return lambda state: any( all(condition(state) for condition in sub_requirement) - for sub_requirement in lambda_conversion + for sub_requirement in fully_converted_rules ) -def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule: +def make_lambda(entity_hex: str, world: "WitnessWorld") -> Optional[CollectionRule]: """ Lambdas are created in a for loop so values need to be captured """ @@ -283,12 +312,15 @@ def set_rules(world: "WitnessWorld") -> None: real_location = location if location in world.player_locations.EVENT_LOCATION_TABLE: - real_location = location[:-7] + entity_hex = world.player_logic.EVENT_ITEM_PAIRS[location][1] + real_location = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location] entity_hex = associated_entity["entity_hex"] rule = make_lambda(entity_hex, world) + if rule is None: + continue location = world.get_location(location) diff --git a/worlds/witness/test/__init__.py b/worlds/witness/test/__init__.py new file mode 100644 index 000000000000..c3b427851af0 --- /dev/null +++ b/worlds/witness/test/__init__.py @@ -0,0 +1,196 @@ +from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union + +from BaseClasses import CollectionState, Entrance, Item, Location, Region + +from test.bases import WorldTestBase +from test.general import gen_steps, setup_multiworld +from test.multiworld.test_multiworlds import MultiworldTestBase + +from .. import WitnessWorld +from ..data.utils import cast_not_none + + +class WitnessTestBase(WorldTestBase): + game = "The Witness" + player: ClassVar[int] = 1 + + world: WitnessWorld + + def can_beat_game_with_items(self, items: Iterable[Item]) -> bool: + """ + Check that the items listed are enough to beat the game. + """ + + state = CollectionState(self.multiworld) + for item in items: + state.collect(item) + return state.multiworld.can_beat_game(state) + + def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance], item_name: str) -> None: + """ + WorldTestBase.assertAccessDependency, but modified & simplified to work with event items + """ + event_items = [item for item in self.multiworld.get_items() if item.name == item_name] + self.assertTrue(event_items, f"Event item {item_name} does not exist.") + + event_locations = [cast_not_none(event_item.location) for event_item in event_items] + + # Checking for an access dependency on an event item requires a bit of extra work, + # as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it. + # So, we temporarily set the access rules of the event locations to be impossible. + original_rules = {event_location.name: event_location.access_rule for event_location in event_locations} + for event_location in event_locations: + event_location.access_rule = lambda _: False + + # We can't use self.assertAccessDependency here, it doesn't work for event items. (As of 2024-06-30) + test_state = self.multiworld.get_all_state(False) + + self.assertFalse(spot.can_reach(test_state), f"{spot.name} is reachable without {item_name}") + + test_state.collect(event_items[0]) + + self.assertTrue(spot.can_reach(test_state), f"{spot.name} is not reachable despite having {item_name}") + + # Restore original access rules. + for event_location in event_locations: + event_location.access_rule = original_rules[event_location.name] + + def assert_location_exists(self, location_name: str, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, also make sure that this (non-event) location COULD exist. + """ + + if strict_check: + self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist") + + try: + self.world.get_location(location_name) + except KeyError: + self.fail(f"Location {location_name} does not exist.") + + def assert_location_does_not_exist(self, location_name: str, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, be explicit about whether the location could exist in the first place. + """ + + if strict_check: + self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist") + + self.assertRaises( + KeyError, + lambda _: self.world.get_location(location_name), + f"Location {location_name} exists, but is not supposed to.", + ) + + def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int]) -> None: + """ + Assert that the specified mapping of items is enough to beat the game, + and that having one less of any item would result in the game being unbeatable. + """ + # Find the actual items + found_items = [item for item in self.multiworld.get_items() if item.name in required_item_counts] + actual_items: Dict[str, List[Item]] = {item_name: [] for item_name in required_item_counts} + for item in found_items: + if len(actual_items[item.name]) < required_item_counts[item.name]: + actual_items[item.name].append(item) + + # Assert that enough items exist in the item pool to satisfy the specified required counts + for item_name, item_objects in actual_items.items(): + self.assertEqual( + len(item_objects), + required_item_counts[item_name], + f"Couldn't find {required_item_counts[item_name]} copies of item {item_name} available in the pool, " + f"only found {len(item_objects)}", + ) + + # assert that multiworld is beatable with the items specified + self.assertTrue( + self.can_beat_game_with_items(item for items in actual_items.values() for item in items), + f"Could not beat game with items: {required_item_counts}", + ) + + # assert that one less copy of any item would result in the multiworld being unbeatable + for item_name, item_objects in actual_items.items(): + with self.subTest(f"Verify cannot beat game with one less copy of {item_name}"): + removed_item = item_objects.pop() + self.assertFalse( + self.can_beat_game_with_items(item for items in actual_items.values() for item in items), + f"Game was beatable despite having {len(item_objects)} copies of {item_name} " + f"instead of the specified {required_item_counts[item_name]}", + ) + item_objects.append(removed_item) + + +class WitnessMultiworldTestBase(MultiworldTestBase): + options_per_world: List[Dict[str, Any]] + common_options: Dict[str, Any] = {} + + def setUp(self) -> None: + """ + Set up a multiworld with multiple players, each using different options. + """ + + self.multiworld = setup_multiworld([WitnessWorld] * len(self.options_per_world), ()) + + for world, options in zip(self.multiworld.worlds.values(), self.options_per_world): + for option_name, option_value in {**self.common_options, **options}.items(): + option = getattr(world.options, option_name) + self.assertIsNotNone(option) + + option.value = option.from_any(option_value).value + + self.assertSteps(gen_steps) + + def collect_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]: + """ + Collect all copies of a specified item name (or list of item names) for a player in the multiworld item pool. + """ + + items = self.get_items_by_name(item_names, player) + for item in items: + self.multiworld.state.collect(item) + return items + + def get_items_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]: + """ + Return all copies of a specified item name (or list of item names) for a player in the multiworld item pool. + """ + + if isinstance(item_names, str): + item_names = (item_names,) + return [item for item in self.multiworld.itempool if item.name in item_names and item.player == player] + + def assert_location_exists(self, location_name: str, player: int, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, also make sure that this (non-event) location COULD exist. + """ + + world = self.multiworld.worlds[player] + + if strict_check: + self.assertIn(location_name, world.location_name_to_id, f"Location {location_name} can never exist") + + try: + world.get_location(location_name) + except KeyError: + self.fail(f"Location {location_name} does not exist.") + + def assert_location_does_not_exist(self, location_name: str, player: int, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, be explicit about whether the location could exist in the first place. + """ + + world = self.multiworld.worlds[player] + + if strict_check: + self.assertIn(location_name, world.location_name_to_id, f"Location {location_name} can never exist") + + self.assertRaises( + KeyError, + lambda _: world.get_location(location_name), + f"Location {location_name} exists, but is not supposed to.", + ) diff --git a/worlds/witness/test/test_auto_elevators.py b/worlds/witness/test/test_auto_elevators.py new file mode 100644 index 000000000000..f91943e85577 --- /dev/null +++ b/worlds/witness/test/test_auto_elevators.py @@ -0,0 +1,50 @@ +from ..test import WitnessMultiworldTestBase + + +class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase): + options_per_world = [ + { + "elevators_come_to_you": {}, + }, + { + "elevators_come_to_you": {"Quarry Elevator", "Swamp Long Bridge", "Bunker Elevator"}, + }, + { + "elevators_come_to_you": {} + }, + ] + + common_options = { + "shuffle_symbols": False, + "shuffle_doors": "panels", + "shuffle_boat": True, + "shuffle_EPs": "individual", + "obelisk_keys": False, + } + + def test_correct_access_per_player(self) -> None: + """ + Test that in a multiworld with players that alternate the elevators_come_to_you option, + the actual behavior alternates as well and doesn't bleed over from slot to slot. + (This is essentially a "does connection info bleed over" test). + """ + + combinations = [ + ("Quarry Elevator Control (Panel)", "Quarry Boathouse Intro Left"), + ("Swamp Long Bridge (Panel)", "Swamp Long Bridge Side EP"), + ("Bunker Elevator Control (Panel)", "Bunker Laser Panel"), + ] + + for item, location in combinations: + with self.subTest(f"Test that {item} only locks {location} for player 2"): + self.assertFalse(self.multiworld.state.can_reach_location(location, 1)) + self.assertFalse(self.multiworld.state.can_reach_location(location, 2)) + self.assertFalse(self.multiworld.state.can_reach_location(location, 3)) + + self.collect_by_name(item, 1) + self.collect_by_name(item, 2) + self.collect_by_name(item, 3) + + self.assertFalse(self.multiworld.state.can_reach_location(location, 1)) + self.assertTrue(self.multiworld.state.can_reach_location(location, 2)) + self.assertFalse(self.multiworld.state.can_reach_location(location, 3)) diff --git a/worlds/witness/test/test_disable_non_randomized.py b/worlds/witness/test/test_disable_non_randomized.py new file mode 100644 index 000000000000..bf285f035d5b --- /dev/null +++ b/worlds/witness/test/test_disable_non_randomized.py @@ -0,0 +1,39 @@ +from ..rules import _has_lasers +from ..test import WitnessTestBase + + +class TestDisableNonRandomized(WitnessTestBase): + run_default_tests = False + + options = { + "disable_non_randomized_puzzles": True, + "shuffle_doors": "panels", + "early_symbol_item": False, + } + + def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None: + """ + Test the different behaviors of the disable_non_randomized mode: + + 1. Unrandomized locations like Orchard Apple Tree 5 are disabled. + 2. Certain doors or lasers that would usually be activated by unrandomized panels depend on event items instead. + 3. These alternate activations are tied to solving Discarded Panels. + """ + + with self.subTest("Test that unrandomized locations are disabled."): + self.assert_location_does_not_exist("Orchard Apple Tree 5") + + with self.subTest("Test that alternate activation trigger events exist."): + self.assert_dependency_on_event_item( + self.world.get_entrance("Town Tower After Third Door to Town Tower Top"), + "Town Tower 4th Door Opens", + ) + + with self.subTest("Test that alternate activation triggers award lasers."): + self.assertFalse(_has_lasers(1, self.world, False)(self.multiworld.state)) + + self.collect_by_name("Triangles") + + # Alternate triggers yield Bunker Laser (Mountainside Discard) and Monastery Laser (Desert Discard) + self.assertTrue(_has_lasers(2, self.world, False)(self.multiworld.state)) + self.assertFalse(_has_lasers(3, self.world, False)(self.multiworld.state)) diff --git a/worlds/witness/test/test_door_shuffle.py b/worlds/witness/test/test_door_shuffle.py new file mode 100644 index 000000000000..d593a84bdb8f --- /dev/null +++ b/worlds/witness/test/test_door_shuffle.py @@ -0,0 +1,50 @@ +from ..test import WitnessMultiworldTestBase, WitnessTestBase + + +class TestIndividualDoors(WitnessTestBase): + options = { + "shuffle_doors": "doors", + "door_groupings": "off", + } + + def test_swamp_laser_shortcut(self) -> None: + """ + Test that Door Shuffle grants early access to Swamp Laser from the back shortcut. + """ + + self.assertTrue(self.get_items_by_name("Swamp Laser Shortcut (Door)")) + + self.assertAccessDependency( + ["Swamp Laser Panel"], + [ + ["Swamp Laser Shortcut (Door)"], + ["Swamp Red Underwater Exit (Door)"], + ], + only_check_listed=True, + ) + + +class TestForbiddenDoors(WitnessMultiworldTestBase): + options_per_world = [ + { + "early_caves": "off", + }, + { + "early_caves": "add_to_pool", + }, + ] + + common_options = { + "shuffle_doors": "panels", + "shuffle_postgame": True, + } + + def test_forbidden_doors(self) -> None: + self.assertTrue( + self.get_items_by_name("Caves Mountain Shortcut (Panel)", 1), + "Caves Mountain Shortcut (Panel) should exist in panels shuffle, but it didn't." + ) + self.assertFalse( + self.get_items_by_name("Caves Mountain Shortcut (Panel)", 2), + "Caves Mountain Shortcut (Panel) should be removed when Early Caves is enabled, but it still exists." + ) diff --git a/worlds/witness/test/test_ep_shuffle.py b/worlds/witness/test/test_ep_shuffle.py new file mode 100644 index 000000000000..342390916675 --- /dev/null +++ b/worlds/witness/test/test_ep_shuffle.py @@ -0,0 +1,54 @@ +from ..test import WitnessTestBase + + +class TestIndividualEPs(WitnessTestBase): + options = { + "shuffle_EPs": "individual", + "EP_difficulty": "normal", + "obelisk_keys": True, + "disable_non_randomized_puzzles": True, + "shuffle_postgame": False, + "victory_condition": "mountain_box_short", + "early_caves": "off", + } + + def test_correct_eps_exist_and_are_locked(self) -> None: + """ + Test that EP locations exist in shuffle_EPs, but only the ones that actually should (based on options) + """ + + # Test Tutorial First Hallways EP as a proxy for "EPs exist at all" + # Don't wrap in a subtest - If this fails, there is no point. + self.assert_location_exists("Tutorial First Hallway EP") + + with self.subTest("Test that disable_non_randomized disables Monastery Garden Left EP"): + self.assert_location_does_not_exist("Monastery Garden Left EP") + + with self.subTest("Test that shuffle_postgame being off disables postgame EPs."): + self.assert_location_does_not_exist("Caves Skylight EP") + + with self.subTest("Test that ep_difficulty being set to normal excludes tedious EPs."): + self.assert_location_does_not_exist("Shipwreck Couch EP") + + with self.subTest("Test that EPs are being locked by Obelisk Keys."): + self.assertAccessDependency(["Desert Sand Snake EP"], [["Desert Obelisk Key"]], True) + + +class TestObeliskSides(WitnessTestBase): + options = { + "shuffle_EPs": "obelisk_sides", + "EP_difficulty": "eclipse", + "shuffle_vault_boxes": True, + "shuffle_postgame": True, + } + + def test_eclipse_required_for_town_side_6(self) -> None: + """ + Test that Obelisk Sides require the appropriate event items from the individual EPs. + Specifically, assert that Town Obelisk Side 6 needs Theater Eclipse EP. + This doubles as a test for Theater Eclipse EP existing with the right options. + """ + + self.assert_dependency_on_event_item( + self.world.get_location("Town Obelisk Side 6"), "Town Obelisk Side 6 - Theater Eclipse EP" + ) diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py new file mode 100644 index 000000000000..5e60dfc52172 --- /dev/null +++ b/worlds/witness/test/test_lasers.py @@ -0,0 +1,218 @@ +from ..test import WitnessTestBase + + +class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase): + options = { + "shuffle_lasers": True, + "puzzle_randomization": "sigma_normal", + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + } + + 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 Sigma Normal Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 2, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Eraser": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "puzzle_randomization": "sigma_expert", + } + + 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 Sigma Expert Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 2, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Eraser": 1, + "Triangles": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "puzzle_randomization": "none", + } + + 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 Vanilla 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, + } + + 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, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "shuffle_symbols": False, + "shuffle_doors": "panels", + "door_groupings": "off", + } + + def test_panels_to_win(self) -> None: + """ + In door panel shuffle , the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires some control panels for each of the Mountain Floors. + """ + + exact_requirement = { + "Desert Laser": 1, + "Town Desert Laser Redirect Control (Panel)": 1, + "Mountain Floor 1 Light Bridge (Panel)": 1, + "Mountain Floor 2 Light Bridge Near (Panel)": 1, + "Mountain Floor 2 Light Bridge Far (Panel)": 1, + "Mountain Floor 2 Elevator Control (Panel)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestDoorsRequiredToWinElevator(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "shuffle_symbols": False, + "shuffle_doors": "doors", + "door_groupings": "off", + } + + def test_doors_to_elevator_paths(self) -> None: + """ + In remote door shuffle, there are three ways to win. + + - Through the normal route (Mountain Entry -> Descend through Mountain -> Reach Bottom Floor) + - Through the Caves using the Caves Shortcuts (Caves -> Reach Bottom Floor) + - Through the Caves via Challenge (Tunnels -> Challenge -> Caves -> Reach Bottom Floor) + """ + + with self.subTest("Test Elevator victory in shuffle_doors through Mountain Entry."): + exact_requirement = { + "Monastery Laser": 1, + "Mountain Floor 1 Exit (Door)": 1, + "Mountain Floor 2 Staircase Near (Door)": 1, + "Mountain Floor 2 Staircase Far (Door)": 1, + "Mountain Floor 2 Exit (Door)": 1, + "Mountain Bottom Floor Giant Puzzle Exit (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + with self.subTest("Test Elevator victory in shuffle_doors through Caves Shortcuts."): + exact_requirement = { + "Monastery Laser": 1, # Elevator Panel itself has a laser lock + "Caves Mountain Shortcut (Door)": 1, + "Caves Entry (Door)": 1, + "Mountain Bottom Floor Rock (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + with self.subTest("Test Elevator victory in shuffle_doors through Tunnels->Challenge->Caves."): + exact_requirement = { + "Monastery Laser": 1, # Elevator Panel itself has a laser lock + "Windmill Entry (Door)": 1, + "Tunnels Theater Shortcut (Door)": 1, + "Tunnels Entry (Door)": 1, + "Challenge Entry (Door)": 1, + "Caves Pillar Door": 1, + "Caves Entry (Door)": 1, + "Mountain Bottom Floor Rock (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) diff --git a/worlds/witness/test/test_panel_hunt.py b/worlds/witness/test/test_panel_hunt.py new file mode 100644 index 000000000000..2f8434802b75 --- /dev/null +++ b/worlds/witness/test/test_panel_hunt.py @@ -0,0 +1,108 @@ +from BaseClasses import CollectionState + +from worlds.witness.test import WitnessMultiworldTestBase, WitnessTestBase + + +class TestMaxPanelHuntMinChecks(WitnessTestBase): + options = { + "victory_condition": "panel_hunt", + "panel_hunt_total": 100, + "panel_hunt_required_percentage": 100, + "panel_hunt_postgame": "disable_anything_locked_by_lasers", + "disable_non_randomized_puzzles": True, + "shuffle_discarded_panels": False, + "shuffle_vault_boxes": False, + } + + def test_correct_panels_were_picked(self) -> None: + with self.subTest("Check that 100 Hunt Panels were actually picked."): + self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", self.player)), 100) + + with self.subTest("Check that 100 Hunt Panels are enough"): + state_100 = CollectionState(self.multiworld) + panel_hunt_item = self.get_item_by_name("+1 Panel Hunt") + + for _ in range(100): + state_100.collect(panel_hunt_item, True) + state_100.sweep_for_advancements([self.world.get_location("Tutorial Gate Open Solved")]) + + self.assertTrue(self.multiworld.completion_condition[self.player](state_100)) + + with self.subTest("Check that 99 Hunt Panels are not enough"): + state_99 = CollectionState(self.multiworld) + panel_hunt_item = self.get_item_by_name("+1 Panel Hunt") + + for _ in range(99): + state_99.collect(panel_hunt_item, True) + state_99.sweep_for_advancements([self.world.get_location("Tutorial Gate Open Solved")]) + + self.assertFalse(self.multiworld.completion_condition[self.player](state_99)) + + +class TestPanelHuntPostgame(WitnessMultiworldTestBase): + options_per_world = [ + { + "panel_hunt_postgame": "everything_is_eligible" + }, + { + "panel_hunt_postgame": "disable_mountain_lasers_locations" + }, + { + "panel_hunt_postgame": "disable_challenge_lasers_locations" + }, + { + "panel_hunt_postgame": "disable_anything_locked_by_lasers" + }, + ] + + common_options = { + "victory_condition": "panel_hunt", + "panel_hunt_total": 40, + + # Make sure we can check for Short vs Long Lasers locations by making Mountain Bottom Floor Discard accessible. + "shuffle_doors": "doors", + "shuffle_discarded_panels": True, + } + + def test_panel_hunt_postgame(self) -> None: + for player_minus_one, options in enumerate(self.options_per_world): + player = player_minus_one + 1 + postgame_option = options["panel_hunt_postgame"] + with self.subTest(f'Test that "{postgame_option}" results in 40 Hunt Panels.'): + self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", player)), 40) + + # Test that the box gets extra checks from panel_hunt_postgame + + with self.subTest('Test that "everything_is_eligible" has no Mountaintop Box Hunt Panels.'): + self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 1, strict_check=False) + self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 1, strict_check=False) + + with self.subTest('Test that "disable_mountain_lasers_locations" has a Hunt Panel for Short, but not Long.'): + self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 2, strict_check=False) + self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 2, strict_check=False) + + with self.subTest('Test that "disable_challenge_lasers_locations" has a Hunt Panel for Long, but not Short.'): + self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 3, strict_check=False) + self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 3, strict_check=False) + + with self.subTest('Test that "disable_anything_locked_by_lasers" has both Mountaintop Box Hunt Panels.'): + self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 4, strict_check=False) + self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 4, strict_check=False) + + # Check panel_hunt_postgame locations get disabled + + with self.subTest('Test that "everything_is_eligible" does not disable any locked-by-lasers panels.'): + self.assert_location_exists("Mountain Floor 1 Right Row 5", 1) + self.assert_location_exists("Mountain Bottom Floor Discard", 1) + + with self.subTest('Test that "disable_mountain_lasers_locations" disables only Shortbox-Locked panels.'): + self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 2) + self.assert_location_exists("Mountain Bottom Floor Discard", 2) + + with self.subTest('Test that "disable_challenge_lasers_locations" disables only Longbox-Locked panels.'): + self.assert_location_exists("Mountain Floor 1 Right Row 5", 3) + self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 3) + + with self.subTest('Test that "everything_is_eligible" disables only Shortbox-Locked panels.'): + self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 4) + self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 4) diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py new file mode 100644 index 000000000000..05f3235a1f4d --- /dev/null +++ b/worlds/witness/test/test_roll_other_options.py @@ -0,0 +1,71 @@ +from ..options import ElevatorsComeToYou +from ..test import WitnessTestBase + +# These are just some random options combinations, just to catch whether I broke anything obvious + + +class TestExpertNonRandomizedEPs(WitnessTestBase): + options = { + "disable_non_randomized": True, + "puzzle_randomization": "sigma_expert", + "shuffle_EPs": "individual", + "ep_difficulty": "eclipse", + "victory_condition": "challenge", + "shuffle_discarded_panels": False, + "shuffle_boat": False, + "shuffle_dog": "off", + } + + +class TestVanillaAutoElevatorsPanels(WitnessTestBase): + options = { + "puzzle_randomization": "none", + "elevators_come_to_you": ElevatorsComeToYou.valid_keys - ElevatorsComeToYou.default, # Opposite of default + "shuffle_doors": "panels", + "victory_condition": "mountain_box_short", + "early_caves": True, + "shuffle_vault_boxes": True, + "mountain_lasers": 11, + "shuffle_dog": "puzzle_skip", + } + + +class TestMiscOptions(WitnessTestBase): + options = { + "death_link": True, + "death_link_amnesty": 3, + "laser_hints": True, + "hint_amount": 40, + "area_hint_percentage": 100, + "vague_hints": "experimental", + } + + +class TestMaxEntityShuffle(WitnessTestBase): + options = { + "shuffle_symbols": False, + "shuffle_doors": "mixed", + "shuffle_EPs": "individual", + "obelisk_keys": True, + "shuffle_lasers": "anywhere", + "victory_condition": "mountain_box_long", + "shuffle_dog": "random_item", + } + + +class TestPostgameGroupedDoors(WitnessTestBase): + options = { + "puzzle_randomization": "umbra_variety", + "shuffle_postgame": True, + "shuffle_discarded_panels": True, + "shuffle_doors": "doors", + "door_groupings": "regional", + "victory_condition": "elevator", + } + + +class TestPostgamePanels(WitnessTestBase): + options = { + "victory_condition": "mountain_box_long", + "shuffle_postgame": True + } diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py new file mode 100644 index 000000000000..3be874f3c0eb --- /dev/null +++ b/worlds/witness/test/test_symbol_shuffle.py @@ -0,0 +1,80 @@ +from ..test import WitnessMultiworldTestBase, WitnessTestBase + + +class TestSymbols(WitnessTestBase): + options = { + "early_symbol_item": False, + } + + def test_progressive_symbols(self) -> None: + """ + Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots, + and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively. + """ + + progressive_dots = self.get_items_by_name("Progressive Dots") + self.assertEqual(len(progressive_dots), 2) + + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertTrue( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + +class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase): + options_per_world = [ + { + "puzzle_randomization": "sigma_normal", + }, + { + "puzzle_randomization": "sigma_expert", + }, + { + "puzzle_randomization": "none", + }, + { + "puzzle_randomization": "umbra_variety", + } + ] + + common_options = { + "shuffle_discarded_panels": True, + "early_symbol_item": False, + } + + def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None: + """ + In sigma_expert, Discarded Panels require Arrows. + In sigma_normal, Discarded Panels require Triangles, and Arrows shouldn't exist at all as an item. + """ + + with self.subTest("Test that Arrows exist only in the expert seed."): + 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/witness/test/test_weird_traversals.py b/worlds/witness/test/test_weird_traversals.py new file mode 100644 index 000000000000..47b69b01fb4a --- /dev/null +++ b/worlds/witness/test/test_weird_traversals.py @@ -0,0 +1,66 @@ +from ..test import WitnessTestBase + + +class TestWeirdTraversalRequirements(WitnessTestBase): + options = { + "shuffle_vault_boxes": True, + "shuffle_symbols": False, + "shuffle_EPs": "individual", + "EP_difficulty": "tedious", + "shuffle_doors": "doors", + "door_groupings": "off", + "puzzle_randomization": "sigma_expert", + } + + def test_weird_traversal_requirements(self) -> None: + """ + Test that Tunnels Theater Flowers EP and Expert PP2 consider all valid paths logically. + """ + + with self.subTest("Tunnels Theater Flowers EP"): + self.assertAccessDependency( + ["Tunnels Theater Flowers EP"], + [ + ["Theater Exit Left (Door)", "Windmill Entry (Door)", "Tunnels Theater Shortcut (Door)"], + ["Theater Exit Right (Door)", "Windmill Entry (Door)", "Tunnels Theater Shortcut (Door)"], + ["Theater Exit Left (Door)", "Tunnels Town Shortcut (Door)"], + ["Theater Exit Right (Door)", "Tunnels Town Shortcut (Door)"], + ["Theater Entry (Door)", "Tunnels Theater Shortcut (Door)"], + ["Theater Entry (Door)", "Windmill Entry (Door)", "Tunnels Town Shortcut (Door)"], + ], + only_check_listed=True, + ) + + with self.subTest("Expert Keep Pressure Plates 2"): + # Always necessary + self.assertAccessDependency( + ["Keep Pressure Plates 2"], + [["Keep Pressure Plates 1 Exit (Door)"]], + only_check_listed=True, + ) + + # Always necessary + self.assertAccessDependency( + ["Keep Pressure Plates 2"], + [["Keep Pressure Plates 3 Exit (Door)"]], + only_check_listed=True, + ) + + # All the possible "Exit methods" from PP3 + self.assertAccessDependency( + ["Keep Pressure Plates 2"], + [ + ["Keep Shadows Shortcut (Door)"], + ["Keep Pressure Plates 4 Exit (Door)", "Keep Tower Shortcut (Door)"], + ["Keep Pressure Plates 4 Exit (Door)", "Keep Hedge Maze 4 Exit (Door)", + "Keep Hedge Maze 4 Shortcut (Door)"], + ["Keep Pressure Plates 4 Exit (Door)", "Keep Hedge Maze 4 Exit (Door)", + "Keep Hedge Maze 3 Exit (Door)", "Keep Hedge Maze 3 Shortcut (Door)"], + ["Keep Pressure Plates 4 Exit (Door)", "Keep Hedge Maze 4 Exit (Door)", + "Keep Hedge Maze 3 Exit (Door)", "Keep Hedge Maze 2 Exit (Door)", + "Keep Hedge Maze 2 Shortcut (Door)"], + ["Keep Pressure Plates 4 Exit (Door)", "Keep Hedge Maze 4 Exit (Door)", + "Keep Hedge Maze 3 Exit (Door)", "Keep Hedge Maze 2 Exit (Door)", "Keep Hedge Maze 1 Exit (Door)"], + ], + only_check_listed=True, + ) diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py new file mode 100644 index 000000000000..d6488498f51a --- /dev/null +++ b/worlds/yachtdice/Items.py @@ -0,0 +1,116 @@ +import typing + +from BaseClasses import Item, ItemClassification + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + classification: ItemClassification + + +class YachtDiceItem(Item): + game: str = "Yacht Dice" + + +# the starting index is chosen semi-randomly to be 16871244000 + + +item_table = { + "Dice": ItemData(16871244000, ItemClassification.progression | ItemClassification.useful), + "Dice Fragment": ItemData(16871244001, ItemClassification.progression), + "Roll": ItemData(16871244002, ItemClassification.progression), + "Roll Fragment": ItemData(16871244003, ItemClassification.progression), + "Fixed Score Multiplier": ItemData(16871244005, ItemClassification.progression), + "Step Score Multiplier": ItemData(16871244006, ItemClassification.progression), + "Category Ones": ItemData(16871244103, ItemClassification.progression), + "Category Twos": ItemData(16871244104, ItemClassification.progression), + "Category Threes": ItemData(16871244105, ItemClassification.progression), + "Category Fours": ItemData(16871244106, ItemClassification.progression), + "Category Fives": ItemData(16871244107, ItemClassification.progression), + "Category Sixes": ItemData(16871244108, ItemClassification.progression), + "Category Choice": ItemData(16871244109, ItemClassification.progression), + "Category Inverse Choice": ItemData(16871244110, ItemClassification.progression), + "Category Pair": ItemData(16871244111, ItemClassification.progression), + "Category Three of a Kind": ItemData(16871244112, ItemClassification.progression), + "Category Four of a Kind": ItemData(16871244113, ItemClassification.progression), + "Category Tiny Straight": ItemData(16871244114, ItemClassification.progression), + "Category Small Straight": ItemData(16871244115, ItemClassification.progression), + "Category Large Straight": ItemData(16871244116, ItemClassification.progression), + "Category Full House": ItemData(16871244117, ItemClassification.progression), + "Category Yacht": ItemData(16871244118, ItemClassification.progression), + "Category Distincts": ItemData(16871244123, ItemClassification.progression), + "Category Two times Ones": ItemData(16871244124, ItemClassification.progression), + "Category Half of Sixes": ItemData(16871244125, ItemClassification.progression), + "Category Twos and Threes": ItemData(16871244126, ItemClassification.progression), + "Category Sum of Odds": ItemData(16871244127, ItemClassification.progression), + "Category Sum of Evens": ItemData(16871244128, ItemClassification.progression), + "Category Double Threes and Fours": ItemData(16871244129, ItemClassification.progression), + "Category Quadruple Ones and Twos": ItemData(16871244130, ItemClassification.progression), + "Category Micro Straight": ItemData(16871244131, ItemClassification.progression), + "Category Three Odds": ItemData(16871244132, ItemClassification.progression), + "Category 1-2-1 Consecutive": ItemData(16871244133, ItemClassification.progression), + "Category Three Distinct Dice": ItemData(16871244134, ItemClassification.progression), + "Category Two Pair": ItemData(16871244135, ItemClassification.progression), + "Category 2-1-2 Consecutive": ItemData(16871244136, ItemClassification.progression), + "Category Five Distinct Dice": ItemData(16871244137, ItemClassification.progression), + "Category 4&5 Full House": ItemData(16871244138, ItemClassification.progression), + # filler items + "Encouragement": ItemData(16871244200, ItemClassification.filler), + "Fun Fact": ItemData(16871244201, ItemClassification.filler), + "Story Chapter": ItemData(16871244202, ItemClassification.filler), + "Good RNG": ItemData(16871244203, ItemClassification.filler), + "Bad RNG": ItemData(16871244204, ItemClassification.trap), + "Bonus Point": ItemData(16871244205, ItemClassification.useful), # not included in logic + # These points are included in the logic and might be necessary to progress. + "1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing), + "10 Points": ItemData(16871244302, ItemClassification.progression), + "100 Points": ItemData(16871244303, ItemClassification.progression | ItemClassification.useful), +} + +# item groups for better hinting +item_groups = { + "Score Multiplier": { + "Step Score Multiplier", + "Fixed Score Multiplier" + }, + "Categories": { + "Category Ones", + "Category Twos", + "Category Threes", + "Category Fours", + "Category Fives", + "Category Sixes", + "Category Choice", + "Category Inverse Choice", + "Category Pair", + "Category Three of a Kind", + "Category Four of a Kind", + "Category Tiny Straight", + "Category Small Straight", + "Category Large Straight", + "Category Full House", + "Category Yacht", + "Category Distincts", + "Category Two times Ones", + "Category Half of Sixes", + "Category Twos and Threes", + "Category Sum of Odds", + "Category Sum of Evens", + "Category Double Threes and Fours", + "Category Quadruple Ones and Twos", + "Category Micro Straight", + "Category Three Odds", + "Category 1-2-1 Consecutive", + "Category Three Distinct Dice", + "Category Two Pair", + "Category 2-1-2 Consecutive", + "Category Five Distinct Dice", + "Category 4&5 Full House", + }, + "Points": { + "100 Points", + "10 Points", + "1 Point", + "Bonus Point" + }, +} diff --git a/worlds/yachtdice/Locations.py b/worlds/yachtdice/Locations.py new file mode 100644 index 000000000000..a9a236466fcc --- /dev/null +++ b/worlds/yachtdice/Locations.py @@ -0,0 +1,79 @@ +import typing + +from BaseClasses import Location + + +class LocData(typing.NamedTuple): + id: int + region: str + score: int + + +class YachtDiceLocation(Location): + game: str = "Yacht Dice" + + def __init__(self, player: int, name: str, score: int, address: typing.Optional[int], parent): + super().__init__(player, name, address, parent) + self.yacht_dice_score = score + + +all_locations = {} +starting_index = 16871244500 # 500 more than the starting index for items (not necessary, but this is what it is now) + + +def all_locations_fun(max_score): + """ + Function that is called when this file is loaded, which loads in ALL possible locations, score 1 to 1000 + """ + return {f"{i} score": LocData(starting_index + i, "Board", i) for i in range(1, max_score + 1)} + + +def ini_locations(goal_score, max_score, number_of_locations, dif, skip_early_locations, number_of_players): + """ + function that loads in all locations necessary for the game, so based on options. + will make sure that goal_score and max_score are included locations + """ + scaling = 2 # parameter that determines how many low-score location there are. + # need more low-score locations or lower difficulties: + if dif == 1: + scaling = 3 + elif dif == 2: + scaling = 2.3 + + scores = [] + # the scores follow the function int( 1 + (percentage ** scaling) * (max_score-1) ) + # however, this will have many low values, sometimes repeating. + # to avoid repeating scores, highest_score keeps tracks of the highest score location + # and the next score will always be at least highest_score + 1 + # note that current_score is at most max_score-1 + highest_score = 0 + start_score = 0 + + if skip_early_locations: + scaling = 1.95 + if number_of_players > 2: + scaling = max(1.2, 2.2 - number_of_players * 0.1) + + for i in range(number_of_locations - 1): + percentage = i / number_of_locations + current_score = int(start_score + 1 + (percentage**scaling) * (max_score - start_score - 2)) + if current_score <= highest_score: + current_score = highest_score + 1 + highest_score = current_score + scores += [current_score] + + if goal_score != max_score: + # if the goal score is not in the list, find the closest one and make it the goal. + if goal_score not in scores: + closest_num = min(scores, key=lambda x: abs(x - goal_score)) + scores[scores.index(closest_num)] = goal_score + + scores += [max_score] + + location_table = {f"{score} score": LocData(starting_index + score, "Board", score) for score in scores} + + return location_table + + +# we need to run this function to initialize all scores from 1 to 1000, even though not all are used +all_locations = all_locations_fun(1000) diff --git a/worlds/yachtdice/Options.py b/worlds/yachtdice/Options.py new file mode 100644 index 000000000000..f311caa5a993 --- /dev/null +++ b/worlds/yachtdice/Options.py @@ -0,0 +1,332 @@ +from dataclasses import dataclass + +from Options import Choice, OptionGroup, PerGameCommonOptions, Range + + +class GameDifficulty(Choice): + """ + Difficulty. This option determines how difficult the scores are to achieve. + Easy: for beginners. No luck required, just roll the dice and have fun. Lower final goal. + Medium: intended difficulty. If you play smart, you will finish the game without any trouble. + Hard: you will need to play smart and be lucky. + Extreme: really hard mode, which requires many brain wrinkles and insane luck. NOT RECOMMENDED FOR MULTIWORLDS. + """ + + display_name = "Game difficulty" + option_easy = 1 + option_medium = 2 + option_hard = 3 + option_extreme = 4 + default = 2 + + +class ScoreForLastCheck(Range): + """ + The items in the item pool will always allow you to reach a score of 1000. + By default, the last check is also at a score of 1000. + However, you can set the score for the last check to be lower. This will make the game shorter and easier. + """ + + display_name = "Score for last check" + range_start = 500 + range_end = 1000 + default = 1000 + + +class ScoreForGoal(Range): + """ + This option determines what score you need to reach to finish the game. + It cannot be higher than the score for the last check (if it is, this option is changed automatically). + """ + + display_name = "Score for goal" + range_start = 500 + range_end = 1000 + default = 777 + + +class MinimalNumberOfDiceAndRolls(Choice): + """ + The minimal number of dice and rolls in the pool. + These are guaranteed, unlike the later items. + You can never get more than 8 dice and 5 rolls. + You start with one dice and one roll. + """ + + display_name = "Minimal number of dice and rolls in pool" + option_5_dice_and_3_rolls = 2 + option_5_dice_and_5_rolls = 3 + option_6_dice_and_4_rolls = 4 + option_7_dice_and_3_rolls = 5 + option_8_dice_and_2_rolls = 6 + default = 2 + + +class NumberDiceFragmentsPerDice(Range): + """ + Dice can be split into fragments, gathering enough will give you an extra dice. + You start with one dice, and there will always be one full dice in the pool. + The other dice are split into fragments, according to this option. + Setting this to 1 fragment per dice just puts "Dice" objects in the pool. + """ + + display_name = "Number of dice fragments per dice" + range_start = 1 + range_end = 5 + default = 4 + + +class NumberRollFragmentsPerRoll(Range): + """ + Rolls can be split into fragments, gathering enough will give you an extra roll. + You start with one roll, and there will always be one full roll in the pool. + The other rolls are split into fragments, according to this option. + Setting this to 1 fragment per roll just puts "Roll" objects in the pool. + """ + + display_name = "Number of roll fragments per roll" + range_start = 1 + range_end = 5 + default = 4 + + +class AlternativeCategories(Range): + """ + There are 16 default categories, but there are also 16 alternative categories. + These alternative categories can be randomly selected to replace the default categories. + They are a little strange, but can give a fun new experience. + In the game, you can hover over categories to check what they do. + This option determines the number of alternative categories in your game. + """ + + display_name = "Number of alternative categories" + range_start = 0 + range_end = 16 + default = 0 + + +class ChanceOfDice(Range): + """ + The item pool is always filled in such a way that you can reach a score of 1000. + Extra progression items are added that will help you on your quest. + You can set the weight for each extra progressive item in the following options. + + Of course, more dice = more points! + """ + + display_name = "Weight of adding Dice" + range_start = 0 + range_end = 100 + default = 5 + + +class ChanceOfRoll(Range): + """ + With more rolls, you will be able to reach higher scores. + """ + + display_name = "Weight of adding Roll" + range_start = 0 + range_end = 100 + default = 20 + + +class ChanceOfFixedScoreMultiplier(Range): + """ + Getting a Fixed Score Multiplier will boost all future scores by 10%. + """ + + display_name = "Weight of adding Fixed Score Multiplier" + range_start = 0 + range_end = 100 + default = 30 + + +class ChanceOfStepScoreMultiplier(Range): + """ + The Step Score Multiplier boosts your multiplier after every roll by 1%, and resets on sheet reset. + So, keep high scoring categories for later to get the most out of them. + By default, this item is not included. It is fun however, you just need to know the above strategy. + """ + + display_name = "Weight of adding Step Score Multiplier" + range_start = 0 + range_end = 100 + default = 0 + + +class ChanceOfDoubleCategory(Range): + """ + This option allows categories to appear multiple times. + Each time you get a category after the first, its score value gets doubled. + """ + + display_name = "Weight of adding Category copy" + range_start = 0 + range_end = 100 + default = 50 + + +class ChanceOfPoints(Range): + """ + Are you tired of rolling dice countless times and tallying up points one by one, all by yourself? + Worry not, as this option will simply add some points items to the item pool! + And getting one of these points items gives you... points! + Imagine how nice it would be to find tons of them. Or even better, having others find them FOR you! + """ + + display_name = "Weight of adding Points" + range_start = 0 + range_end = 100 + default = 20 + + +class PointsSize(Choice): + """ + If you choose to add points to the item pool, you can choose to have many small points, + medium-size points, a few larger points, or a mix of them. + """ + + display_name = "Size of points" + option_small = 1 + option_medium = 2 + option_large = 3 + option_mix = 4 + default = 2 + + +class MinimizeExtraItems(Choice): + """ + Besides necessary items, Yacht Dice has extra useful/filler items in the item pool. + It is possible however to decrease the number of locations and extra items. + This option will: + - decrease the number of locations at the start (you'll start with 2 dice and 2 rolls). + - will limit the number of dice/roll fragments per dice/roll to 2. + - in multiplayer games, it will reduce the number of filler items. + """ + + display_name = "Minimize extra items" + option_no_dont = 1 + option_yes_please = 2 + default = 1 + + +class AddExtraPoints(Choice): + """ + Yacht Dice typically has space for extra items. + This option determines if bonus points are put into the item pool. + They make the game a little bit easier, as they are not considered in the logic. + + All Of It: fill all locations with extra points + Sure: put some bonus points in + Never: do not put any bonus points + """ + + display_name = "Extra bonus in the pool" + option_all_of_it = 1 + option_sure = 2 + option_never = 3 + default = 2 + + +class AddStoryChapters(Choice): + """ + Yacht Dice typically has space for more items. + This option determines if extra story chapters are put into the item pool. + Note: if you have extra points on "all_of_it", there will not be story chapters. + + All Of It: fill all locations with story chapters + Sure: if there is space left, put in 10 story chapters. + Never: do not put any story chapters in, I do not like reading (but I am glad you are reading THIS!) + """ + + display_name = "Extra story chapters in the pool" + option_all_of_it = 1 + option_sure = 2 + option_never = 3 + default = 3 + + +class WhichStory(Choice): + """ + The most important part of Yacht Dice is the narrative. + Of course you will need to add story chapters to the item pool. + You can read story chapters in the feed on the website and there are several stories to choose from. + """ + + display_name = "Story" + option_the_quest_of_the_dice_warrior = 1 + option_the_tragedy_of_fortunas_gambit = 2 + option_the_dicey_animal_dice_game = 3 + option_whispers_of_fate = 4 + option_a_yacht_dice_odyssey = 5 + option_a_rollin_rhyme_adventure = 6 + option_random_story = -1 + default = -1 + + +class AllowManual(Choice): + """ + If allowed, players can roll IRL dice and input them manually into the game. + By sending "manual" in the chat, an input field appears where you can type your dice rolls. + Of course, we cannot check anymore if the player is playing fair. + """ + + display_name = "Allow manual inputs" + option_yes_allow = 1 + option_no_dont_allow = 2 + default = 1 + + +@dataclass +class YachtDiceOptions(PerGameCommonOptions): + game_difficulty: GameDifficulty + score_for_last_check: ScoreForLastCheck + score_for_goal: ScoreForGoal + + minimal_number_of_dice_and_rolls: MinimalNumberOfDiceAndRolls + number_of_dice_fragments_per_dice: NumberDiceFragmentsPerDice + number_of_roll_fragments_per_roll: NumberRollFragmentsPerRoll + + alternative_categories: AlternativeCategories + + allow_manual_input: AllowManual + + # the following options determine what extra items are shuffled into the pool: + weight_of_dice: ChanceOfDice + weight_of_roll: ChanceOfRoll + weight_of_fixed_score_multiplier: ChanceOfFixedScoreMultiplier + weight_of_step_score_multiplier: ChanceOfStepScoreMultiplier + weight_of_double_category: ChanceOfDoubleCategory + weight_of_points: ChanceOfPoints + points_size: PointsSize + + minimize_extra_items: MinimizeExtraItems + add_bonus_points: AddExtraPoints + add_story_chapters: AddStoryChapters + which_story: WhichStory + + +yd_option_groups = [ + OptionGroup( + "Extra progression items", + [ + ChanceOfDice, + ChanceOfRoll, + ChanceOfFixedScoreMultiplier, + ChanceOfStepScoreMultiplier, + ChanceOfDoubleCategory, + ChanceOfPoints, + PointsSize, + ], + ), + OptionGroup( + "Other items", + [ + MinimizeExtraItems, + AddExtraPoints, + AddStoryChapters, + WhichStory + ], + ), +] diff --git a/worlds/yachtdice/Rules.py b/worlds/yachtdice/Rules.py new file mode 100644 index 000000000000..3fb712fdca09 --- /dev/null +++ b/worlds/yachtdice/Rules.py @@ -0,0 +1,240 @@ +import math +from collections import Counter, defaultdict +from typing import List, Optional + +from BaseClasses import MultiWorld + +from worlds.generic.Rules import set_rule + +from .YachtWeights import yacht_weights + +# This module adds logic to the apworld. +# In short, we ran a simulation for every possible combination of dice and rolls you can have, per category. +# This simulation has a good strategy for locking dice. +# This gives rise to an approximate discrete distribution per category. +# We calculate the distribution of the total score. +# We then pick a correct percentile to reflect the correct score that should be in logic. +# The score is logic is *much* lower than the actual maximum reachable score. + + +class Category: + def __init__(self, name, quantity=1): + self.name = name + self.quantity = quantity # how many times you have the category + + # return mean score of a category + def mean_score(self, num_dice, num_rolls): + if num_dice <= 0 or num_rolls <= 0: + return 0 + 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 + + +class ListState: + def __init__(self, state: List[str]): + self.state = state + self.item_counts = Counter(state) + + def count(self, item: str, player: Optional[str] = None) -> int: + return self.item_counts[item] + + +def extract_progression(state, player, frags_per_dice, frags_per_roll, allowed_categories): + """ + method to obtain a list of what items the player has. + this includes categories, dice, rolls and score multiplier etc. + First, we convert the state if it's a list, so we can use state.count(item, player) + """ + if isinstance(state, list): + state = ListState(state=state) + + number_of_dice = state.count("Dice", player) + state.count("Dice Fragment", player) // frags_per_dice + number_of_rerolls = state.count("Roll", player) + state.count("Roll Fragment", player) // frags_per_roll + number_of_fixed_mults = state.count("Fixed Score Multiplier", player) + number_of_step_mults = state.count("Step Score Multiplier", player) + + categories = [ + Category(category_name, state.count(category_name, player)) + for category_name in allowed_categories + if state.count(category_name, player) # want all categories that have count >= 1 + ] + + extra_points_in_logic = state.count("1 Point", player) + extra_points_in_logic += state.count("10 Points", player) * 10 + extra_points_in_logic += state.count("100 Points", player) * 100 + + return ( + categories, + number_of_dice, + number_of_rerolls, + number_of_fixed_mults * 0.1, + number_of_step_mults * 0.01, + extra_points_in_logic, + ) + + +# We will store the results of this function as it is called often for the same parameters. + + +yachtdice_cache = {} + + +def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, diff, player): + """ + Function that returns the feasible score in logic based on items obtained. + """ + tup = ( + tuple([c.name + str(c.quantity) for c in categories]), + num_dice, + num_rolls, + fixed_mult, + step_mult, + diff, + ) # identifier + + if player not in yachtdice_cache: + yachtdice_cache[player] = {} + + if tup in yachtdice_cache[player]: + return yachtdice_cache[player][tup] + + # sort categories because for the step multiplier, you will want low-scoring categories first + # to avoid errors with order changing when obtaining rolls, we order assuming 4 rolls + categories.sort(key=lambda category: category.mean_score(num_dice, 4)) + + # function to add two discrete distribution. + # defaultdict is a dict where you don't need to check if an id is present, you can just use += (lot faster) + def add_distributions(dist1, dist2): + combined_dist = defaultdict(float) + for val2, prob2 in dist2.items(): + for val1, prob1 in dist1.items(): + combined_dist[val1 + val2] += prob1 * prob2 + return dict(combined_dist) + + # function to take the maximum of "times" i.i.d. dist1. + # (I have tried using defaultdict here too but this made it slower.) + def max_dist(dist1, mults): + new_dist = {0: 1} + for mult in mults: + temp_dist = {} + for val1, prob1 in new_dist.items(): + for val2, prob2 in dist1.items(): + new_val = int(max(val1, val2 * mult)) + new_prob = prob1 * prob2 + + # Update the probability for the new value + if new_val in temp_dist: + temp_dist[new_val] += new_prob + else: + temp_dist[new_val] = new_prob + new_dist = temp_dist + + return new_dist + + # Returns percentile value of a distribution. + def percentile_distribution(dist, percentile): + sorted_values = sorted(dist.keys()) + cumulative_prob = 0 + + for val in sorted_values: + cumulative_prob += dist[val] + if cumulative_prob >= percentile: + return val + + # Return the last value if percentile is higher than all probabilities + return sorted_values[-1] + + # parameters for logic. + # perc_return is, per difficulty, the percentages of total score it returns (it averages out the values) + # diff_divide determines how many shots the logic gets per category. Lower = more shots. + perc_return = [[0], [0.1, 0.5], [0.3, 0.7], [0.55, 0.85], [0.85, 0.95]][diff] + diff_divide = [0, 9, 7, 3, 2][diff] + + # calculate total distribution + total_dist = {0: 1} + for j, category in enumerate(categories): + if num_dice <= 0 or num_rolls <= 0: + dist = {0: 100000} + else: + dist = yacht_weights[category.name, min(8, num_dice), min(8, num_rolls)].copy() + + for key in dist.keys(): + dist[key] /= 100000 + + cat_mult = 2 ** (category.quantity - 1) + + # for higher difficulties, the simulation gets multiple tries for categories. + max_tries = j // diff_divide + mults = [(1 + fixed_mult + step_mult * ii) * cat_mult for ii in range(max(0, j - max_tries), j + 1)] + dist = max_dist(dist, mults) + + total_dist = add_distributions(total_dist, dist) + + # save result into the cache, then return it + outcome = sum([percentile_distribution(total_dist, perc) for perc in perc_return]) / len(perc_return) + yachtdice_cache[player][tup] = max(5, math.floor(outcome)) # at least 5. + + # cache management; we rarely/never need more than 400 entries. But if for some reason it became large, + # delete the first entry of the player cache. + if len(yachtdice_cache[player]) > 400: + # Remove the oldest item + oldest_tup = next(iter(yachtdice_cache[player])) + del yachtdice_cache[player][oldest_tup] + + return yachtdice_cache[player][tup] + + +def dice_simulation_fill_pool(state, frags_per_dice, frags_per_roll, allowed_categories, difficulty, player): + """ + Returns the feasible score that one can reach with the current state, options and difficulty. + This function is called with state being a list, during filling of item pool. + """ + categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression( + state, "state_is_a_list", frags_per_dice, frags_per_roll, allowed_categories + ) + return ( + dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, difficulty, player) + expoints + ) + + +def dice_simulation_state_change(state, player, frags_per_dice, frags_per_roll, allowed_categories, difficulty): + """ + Returns the feasible score that one can reach with the current state, options and difficulty. + This function is called with state being a AP state object, while doing access rules. + """ + + if state.prog_items[player]["state_is_fresh"] == 0: + state.prog_items[player]["state_is_fresh"] = 1 + categories, num_dice, num_rolls, fixed_mult, step_mult, expoints = extract_progression( + state, player, frags_per_dice, frags_per_roll, allowed_categories + ) + state.prog_items[player]["maximum_achievable_score"] = ( + dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mult, difficulty, player) + + expoints + ) + + return state.prog_items[player]["maximum_achievable_score"] + + +def set_yacht_rules(world: MultiWorld, player: int, frags_per_dice, frags_per_roll, allowed_categories, difficulty): + """ + Sets rules on reaching scores + """ + + for location in world.get_locations(player): + set_rule( + location, + lambda state, curscore=location.yacht_dice_score, player=player: dice_simulation_state_change( + state, player, frags_per_dice, frags_per_roll, allowed_categories, difficulty + ) + >= curscore, + ) + + +def set_yacht_completion_rules(world: MultiWorld, player: int): + """ + Sets rules on completion condition + """ + world.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/yachtdice/YachtWeights.py b/worlds/yachtdice/YachtWeights.py new file mode 100644 index 000000000000..f18766d9498a --- /dev/null +++ b/worlds/yachtdice/YachtWeights.py @@ -0,0 +1,2356 @@ +yacht_weights = { + ("Category Ones", 0, 0): {0: 100000}, + ("Category Ones", 0, 1): {0: 100000}, + ("Category Ones", 0, 2): {0: 100000}, + ("Category Ones", 0, 3): {0: 100000}, + ("Category Ones", 0, 4): {0: 100000}, + ("Category Ones", 0, 5): {0: 100000}, + ("Category Ones", 0, 6): {0: 100000}, + ("Category Ones", 0, 7): {0: 100000}, + ("Category Ones", 0, 8): {0: 100000}, + ("Category Ones", 1, 0): {0: 100000}, + ("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: 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, 1: 83964}, + ("Category Ones", 2, 6): {0: 11355, 1: 88645}, + ("Category Ones", 2, 7): {0: 7812, 1: 92188}, + ("Category Ones", 2, 8): {0: 5395, 1: 94605}, + ("Category Ones", 3, 0): {0: 100000}, + ("Category Ones", 3, 1): {0: 100000}, + ("Category Ones", 3, 2): {0: 33327, 1: 66673}, + ("Category Ones", 3, 3): {0: 19432, 1: 80568}, + ("Category Ones", 3, 4): {0: 11191, 1: 88809}, + ("Category Ones", 3, 5): {0: 3963, 2: 64583, 1: 31454}, + ("Category Ones", 3, 6): {0: 3286, 2: 96714}, + ("Category Ones", 3, 7): {0: 57, 2: 99943}, + ("Category Ones", 3, 8): {2: 100000}, + ("Category Ones", 4, 0): {0: 100000}, + ("Category Ones", 4, 1): {0: 100000}, + ("Category Ones", 4, 2): {0: 23349, 1: 76651}, + ("Category Ones", 4, 3): {0: 11366, 1: 88634}, + ("Category Ones", 4, 4): {0: 3246, 2: 71438, 1: 25316}, + ("Category Ones", 4, 5): {0: 1466, 2: 98534}, + ("Category Ones", 4, 6): {0: 7, 2: 99993}, + ("Category Ones", 4, 7): {0: 2, 2: 31222, 3: 68776}, + ("Category Ones", 4, 8): {3: 99999, 2: 1}, + ("Category Ones", 5, 0): {0: 100000}, + ("Category Ones", 5, 1): {0: 100000}, + ("Category Ones", 5, 2): {0: 16212, 1: 83788}, + ("Category Ones", 5, 3): {0: 4879, 2: 69906, 1: 25215}, + ("Category Ones", 5, 4): {0: 1513, 2: 98487}, + ("Category Ones", 5, 5): {0: 484, 2: 31541, 3: 67975}, + ("Category Ones", 5, 6): {3: 99785, 2: 215}, + ("Category Ones", 5, 7): {3: 100000}, + ("Category Ones", 5, 8): {4: 66815, 3: 33185}, + ("Category Ones", 6, 0): {0: 100000}, + ("Category Ones", 6, 1): {0: 33501, 1: 66499}, + ("Category Ones", 6, 2): {0: 11326, 1: 88674}, + ("Category Ones", 6, 3): {0: 2289, 2: 79783, 1: 17928}, + ("Category Ones", 6, 4): {0: 10, 3: 68933, 2: 30973, 1: 84}, + ("Category Ones", 6, 5): {0: 4, 3: 99996}, + ("Category Ones", 6, 6): {2: 1, 4: 67785, 3: 32214}, + ("Category Ones", 6, 7): {4: 100000}, + ("Category Ones", 6, 8): {4: 100000}, + ("Category Ones", 7, 0): {0: 100000}, + ("Category Ones", 7, 1): {0: 27838, 1: 72162}, + ("Category Ones", 7, 2): {0: 8807, 2: 68364, 1: 22829}, + ("Category Ones", 7, 3): {0: 75, 3: 62348, 2: 35246, 1: 2331}, + ("Category Ones", 7, 4): {0: 6, 3: 99994}, + ("Category Ones", 7, 5): {3: 29500, 4: 70500}, + ("Category Ones", 7, 6): {4: 100000}, + ("Category Ones", 7, 7): {4: 30322, 5: 69678}, + ("Category Ones", 7, 8): {5: 100000}, + ("Category Ones", 8, 0): {0: 100000}, + ("Category Ones", 8, 1): {0: 23156, 1: 76844}, + ("Category Ones", 8, 2): {0: 5678, 2: 75480, 1: 18842}, + ("Category Ones", 8, 3): {0: 28, 3: 99972}, + ("Category Ones", 8, 4): {3: 32486, 4: 67514}, + ("Category Ones", 8, 5): {4: 100000}, + ("Category Ones", 8, 6): {5: 74125, 4: 25875}, + ("Category Ones", 8, 7): {6: 60476, 5: 29297, 4: 10227}, + ("Category Ones", 8, 8): {6: 99999, 5: 1}, + ("Category Twos", 0, 0): {0: 100000}, + ("Category Twos", 0, 1): {0: 100000}, + ("Category Twos", 0, 2): {0: 100000}, + ("Category Twos", 0, 3): {0: 100000}, + ("Category Twos", 0, 4): {0: 100000}, + ("Category Twos", 0, 5): {0: 100000}, + ("Category Twos", 0, 6): {0: 100000}, + ("Category Twos", 0, 7): {0: 100000}, + ("Category Twos", 0, 8): {0: 100000}, + ("Category Twos", 1, 0): {0: 100000}, + ("Category Twos", 1, 1): {0: 100000}, + ("Category Twos", 1, 2): {0: 69690, 2: 30310}, + ("Category Twos", 1, 3): {0: 57818, 2: 42182}, + ("Category Twos", 1, 4): {0: 48418, 2: 51582}, + ("Category Twos", 1, 5): {0: 40301, 2: 59699}, + ("Category Twos", 1, 6): {0: 33558, 2: 66442}, + ("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: 51762}, + ("Category Twos", 2, 3): {0: 33290, 2: 66710}, + ("Category Twos", 2, 4): {0: 23136, 2: 76864}, + ("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", 3, 0): {0: 100000}, + ("Category Twos", 3, 1): {0: 58021, 2: 41979}, + ("Category Twos", 3, 2): {0: 33548, 2: 66452}, + ("Category Twos", 3, 3): {0: 19375, 2: 42372, 4: 38253}, + ("Category Twos", 3, 4): {0: 10998, 2: 36435, 4: 52567}, + ("Category Twos", 3, 5): {0: 7954, 4: 92046}, + ("Category Twos", 3, 6): {0: 347, 4: 99653}, + ("Category Twos", 3, 7): {0: 2, 4: 62851, 6: 37147}, + ("Category Twos", 3, 8): {6: 99476, 4: 524}, + ("Category Twos", 4, 0): {0: 100000}, + ("Category Twos", 4, 1): {0: 48235, 2: 51765}, + ("Category Twos", 4, 2): {0: 23289, 2: 40678, 4: 36033}, + ("Category Twos", 4, 3): {0: 11177, 2: 32677, 4: 56146}, + ("Category Twos", 4, 4): {0: 5522, 4: 60436, 6: 34042}, + ("Category Twos", 4, 5): {0: 4358, 6: 95642}, + ("Category Twos", 4, 6): {0: 20, 6: 99980}, + ("Category Twos", 4, 7): {6: 100000}, + ("Category Twos", 4, 8): {6: 65250, 8: 34750}, + ("Category Twos", 5, 0): {0: 100000}, + ("Category Twos", 5, 1): {0: 40028, 2: 59972}, + ("Category Twos", 5, 2): {0: 16009, 2: 35901, 4: 48090}, + ("Category Twos", 5, 3): {0: 6820, 4: 57489, 6: 35691}, + ("Category Twos", 5, 4): {0: 5285, 6: 94715}, + ("Category Twos", 5, 5): {0: 18, 6: 66613, 8: 33369}, + ("Category Twos", 5, 6): {8: 99073, 6: 927}, + ("Category Twos", 5, 7): {8: 100000}, + ("Category Twos", 5, 8): {8: 100000}, + ("Category Twos", 6, 0): {0: 100000}, + ("Category Twos", 6, 1): {0: 33502, 2: 66498}, + ("Category Twos", 6, 2): {0: 13681, 4: 59162, 2: 27157}, + ("Category Twos", 6, 3): {0: 5486, 6: 94514}, + ("Category Twos", 6, 4): {0: 190, 6: 62108, 8: 37702}, + ("Category Twos", 6, 5): {8: 99882, 6: 118}, + ("Category Twos", 6, 6): {8: 65144, 10: 34856}, + ("Category Twos", 6, 7): {10: 99524, 8: 476}, + ("Category Twos", 6, 8): {10: 100000}, + ("Category Twos", 7, 0): {0: 100000}, + ("Category Twos", 7, 1): {0: 27683, 2: 39060, 4: 33257}, + ("Category Twos", 7, 2): {0: 8683, 4: 54932, 6: 36385}, + ("Category Twos", 7, 3): {0: 373, 6: 66572, 8: 33055}, + ("Category Twos", 7, 4): {8: 99816, 6: 184}, + ("Category Twos", 7, 5): {8: 58124, 10: 41876}, + ("Category Twos", 7, 6): {10: 99948, 8: 52}, + ("Category Twos", 7, 7): {10: 62549, 12: 37451}, + ("Category Twos", 7, 8): {12: 99818, 10: 182}, + ("Category Twos", 8, 0): {0: 100000}, + ("Category Twos", 8, 1): {0: 23378, 2: 37157, 4: 39465}, + ("Category Twos", 8, 2): {0: 5602, 6: 94398}, + ("Category Twos", 8, 3): {0: 8, 6: 10911, 8: 89081}, + ("Category Twos", 8, 4): {8: 59809, 10: 40191}, + ("Category Twos", 8, 5): {10: 68808, 12: 31114, 8: 78}, + ("Category Twos", 8, 6): {12: 98712, 10: 1287, 8: 1}, + ("Category Twos", 8, 7): {12: 100000}, + ("Category Twos", 8, 8): {12: 59018, 14: 40982}, + ("Category Threes", 0, 0): {0: 100000}, + ("Category Threes", 0, 1): {0: 100000}, + ("Category Threes", 0, 2): {0: 100000}, + ("Category Threes", 0, 3): {0: 100000}, + ("Category Threes", 0, 4): {0: 100000}, + ("Category Threes", 0, 5): {0: 100000}, + ("Category Threes", 0, 6): {0: 100000}, + ("Category Threes", 0, 7): {0: 100000}, + ("Category Threes", 0, 8): {0: 100000}, + ("Category Threes", 1, 0): {0: 100000}, + ("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}, + ("Category Threes", 1, 5): {0: 40271, 3: 59729}, + ("Category Threes", 1, 6): {0: 33201, 3: 66799}, + ("Category Threes", 1, 7): {0: 27903, 3: 72097}, + ("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: 51798}, + ("Category Threes", 2, 3): {0: 33376, 3: 66624}, + ("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", 3, 0): {0: 100000}, + ("Category Threes", 3, 1): {0: 57964, 3: 42036}, + ("Category Threes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, + ("Category Threes", 3, 3): {0: 19520, 3: 42382, 6: 38098}, + ("Category Threes", 3, 4): {0: 11265, 3: 35772, 6: 52963}, + ("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: 1317, 6: 30047, 9: 68636}, + ("Category Threes", 3, 8): {0: 750, 9: 99250}, + ("Category Threes", 4, 0): {0: 100000}, + ("Category Threes", 4, 1): {0: 48121, 3: 51879}, + ("Category Threes", 4, 2): {0: 23296, 3: 40989, 6: 35715}, + ("Category Threes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, + ("Category Threes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 33799}, + ("Category Threes", 4, 5): {0: 5225, 6: 29678, 9: 65097}, + ("Category Threes", 4, 6): {0: 3535, 9: 96465}, + ("Category Threes", 4, 7): {0: 6, 9: 72939, 12: 27055}, + ("Category Threes", 4, 8): {9: 25326, 12: 74674}, + ("Category Threes", 5, 0): {0: 100000}, + ("Category Threes", 5, 1): {0: 40183, 3: 59817}, + ("Category Threes", 5, 2): {0: 16197, 3: 35494, 6: 48309}, + ("Category Threes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 35591}, + ("Category Threes", 5, 4): {0: 5007, 6: 25159, 9: 49038, 12: 20796}, + ("Category Threes", 5, 5): {0: 2900, 9: 38935, 12: 58165}, + ("Category Threes", 5, 6): {0: 2090, 12: 97910}, + ("Category Threes", 5, 7): {12: 99994, 9: 6}, + ("Category Threes", 5, 8): {12: 73524, 15: 26476}, + ("Category Threes", 6, 0): {0: 100000}, + ("Category Threes", 6, 1): {0: 33473, 3: 40175, 6: 26352}, + ("Category Threes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 26631}, + ("Category Threes", 6, 3): {0: 2460, 6: 21148, 9: 55356, 12: 21036}, + ("Category Threes", 6, 4): {0: 997, 9: 29741, 12: 69262}, + ("Category Threes", 6, 5): {0: 831, 12: 76328, 15: 22841}, + ("Category Threes", 6, 6): {12: 29960, 15: 70040}, + ("Category Threes", 6, 7): {15: 100000}, + ("Category Threes", 6, 8): {15: 79456, 18: 20544}, + ("Category Threes", 7, 0): {0: 100000}, + ("Category Threes", 7, 1): {0: 27933, 3: 39105, 6: 32962}, + ("Category Threes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 36478}, + ("Category Threes", 7, 3): {0: 1321, 9: 40251, 12: 58428}, + ("Category Threes", 7, 4): {0: 370, 12: 74039, 15: 25591}, + ("Category Threes", 7, 5): {0: 6, 15: 98660, 12: 1334}, + ("Category Threes", 7, 6): {15: 73973, 18: 26027}, + ("Category Threes", 7, 7): {18: 100000}, + ("Category Threes", 7, 8): {18: 100000}, + ("Category Threes", 8, 0): {0: 100000}, + ("Category Threes", 8, 1): {0: 23337, 3: 37232, 6: 39431}, + ("Category Threes", 8, 2): {0: 4652, 6: 29310, 9: 45517, 12: 20521}, + ("Category Threes", 8, 3): {0: 1300, 12: 77919, 15: 20781}, + ("Category Threes", 8, 4): {0: 21, 15: 98678, 12: 1301}, + ("Category Threes", 8, 5): {15: 68893, 18: 31107}, + ("Category Threes", 8, 6): {18: 100000}, + ("Category Threes", 8, 7): {18: 69986, 21: 30014}, + ("Category Threes", 8, 8): {21: 98839, 18: 1161}, + ("Category Fours", 0, 0): {0: 100000}, + ("Category Fours", 0, 1): {0: 100000}, + ("Category Fours", 0, 2): {0: 100000}, + ("Category Fours", 0, 3): {0: 100000}, + ("Category Fours", 0, 4): {0: 100000}, + ("Category Fours", 0, 5): {0: 100000}, + ("Category Fours", 0, 6): {0: 100000}, + ("Category Fours", 0, 7): {0: 100000}, + ("Category Fours", 0, 8): {0: 100000}, + ("Category Fours", 1, 0): {0: 100000}, + ("Category Fours", 1, 1): {0: 83260, 4: 16740}, + ("Category Fours", 1, 2): {0: 69514, 4: 30486}, + ("Category Fours", 1, 3): {0: 58017, 4: 41983}, + ("Category Fours", 1, 4): {0: 48389, 4: 51611}, + ("Category Fours", 1, 5): {0: 40201, 4: 59799}, + ("Category Fours", 1, 6): {0: 33496, 4: 66504}, + ("Category Fours", 1, 7): {0: 28052, 4: 71948}, + ("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: 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", 3, 0): {0: 100000}, + ("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: 38422}, + ("Category Fours", 3, 4): {0: 11125, 4: 36011, 8: 52864}, + ("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: 488, 8: 20703, 12: 78809}, + ("Category Fours", 4, 0): {0: 100000}, + ("Category Fours", 4, 1): {0: 48465, 4: 51535}, + ("Category Fours", 4, 2): {0: 23296, 4: 40911, 8: 35793}, + ("Category Fours", 4, 3): {0: 11200, 4: 33191, 8: 35337, 12: 20272}, + ("Category Fours", 4, 4): {0: 5447, 4: 23066, 8: 37441, 12: 34046}, + ("Category Fours", 4, 5): {0: 2533, 4: 15668, 8: 34781, 12: 47018}, + ("Category Fours", 4, 6): {0: 2058, 8: 19749, 12: 58777, 16: 19416}, + ("Category Fours", 4, 7): {0: 1476, 12: 45913, 16: 52611}, + ("Category Fours", 4, 8): {0: 23, 12: 18149, 16: 81828}, + ("Category Fours", 5, 0): {0: 100000}, + ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 19658}, + ("Category Fours", 5, 2): {0: 15946, 4: 35579, 8: 31158, 12: 17317}, + ("Category Fours", 5, 3): {0: 6479, 4: 23705, 8: 34575, 12: 35241}, + ("Category Fours", 5, 4): {0: 4987, 8: 25190, 12: 48849, 16: 20974}, + ("Category Fours", 5, 5): {0: 1553, 12: 39966, 16: 58481}, + ("Category Fours", 5, 6): {0: 843, 16: 99157}, + ("Category Fours", 5, 7): {16: 80514, 20: 19486}, + ("Category Fours", 5, 8): {16: 38393, 20: 61607}, + ("Category Fours", 6, 0): {0: 100000}, + ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 26512}, + ("Category Fours", 6, 2): {0: 11175, 4: 29824, 8: 32381, 12: 26620}, + ("Category Fours", 6, 3): {0: 3698, 4: 16329, 8: 29939, 12: 29071, 16: 20963}, + ("Category Fours", 6, 4): {0: 2326, 12: 28286, 16: 69388}, + ("Category Fours", 6, 5): {0: 1030, 16: 76056, 20: 22914}, + ("Category Fours", 6, 6): {0: 7, 16: 29753, 20: 70240}, + ("Category Fours", 6, 7): {20: 99999, 16: 1}, + ("Category Fours", 6, 8): {20: 79470, 24: 20530}, + ("Category Fours", 7, 0): {0: 100000}, + ("Category Fours", 7, 1): {0: 27821, 4: 39289, 8: 32890}, + ("Category Fours", 7, 2): {0: 7950, 4: 24026, 8: 31633, 12: 36391}, + ("Category Fours", 7, 3): {0: 1887, 12: 31108, 16: 67005}, + ("Category Fours", 7, 4): {0: 423, 16: 73837, 20: 25740}, + ("Category Fours", 7, 5): {0: 57, 16: 10063, 20: 74092, 24: 15788}, + ("Category Fours", 7, 6): {0: 6, 20: 31342, 24: 68652}, + ("Category Fours", 7, 7): {24: 99995, 20: 5}, + ("Category Fours", 7, 8): {24: 84330, 28: 15670}, + ("Category Fours", 8, 0): {0: 100000}, + ("Category Fours", 8, 1): {0: 23275, 4: 37161, 8: 39564}, + ("Category Fours", 8, 2): {0: 5421, 4: 19014, 8: 29259, 12: 25812, 16: 20494}, + ("Category Fours", 8, 3): {0: 649, 16: 78572, 20: 20779}, + ("Category Fours", 8, 4): {0: 15, 20: 80772, 24: 17355, 16: 1858}, + ("Category Fours", 8, 5): {20: 15615, 24: 84385}, + ("Category Fours", 8, 6): {24: 80655, 28: 19345}, + ("Category Fours", 8, 7): {24: 23969, 28: 76031}, + ("Category Fours", 8, 8): {28: 100000}, + ("Category Fives", 0, 0): {0: 100000}, + ("Category Fives", 0, 1): {0: 100000}, + ("Category Fives", 0, 2): {0: 100000}, + ("Category Fives", 0, 3): {0: 100000}, + ("Category Fives", 0, 4): {0: 100000}, + ("Category Fives", 0, 5): {0: 100000}, + ("Category Fives", 0, 6): {0: 100000}, + ("Category Fives", 0, 7): {0: 100000}, + ("Category Fives", 0, 8): {0: 100000}, + ("Category Fives", 1, 0): {0: 100000}, + ("Category Fives", 1, 1): {0: 83338, 5: 16662}, + ("Category Fives", 1, 2): {0: 69499, 5: 30501}, + ("Category Fives", 1, 3): {0: 57799, 5: 42201}, + ("Category Fives", 1, 4): {0: 48311, 5: 51689}, + ("Category Fives", 1, 5): {0: 40084, 5: 59916}, + ("Category Fives", 1, 6): {0: 33440, 5: 66560}, + ("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: 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}, + ("Category Fives", 2, 6): {0: 11340, 5: 44324, 10: 44336}, + ("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: 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", 4, 0): {0: 100000}, + ("Category Fives", 4, 1): {0: 48377, 5: 38345, 10: 13278}, + ("Category Fives", 4, 2): {0: 23126, 5: 40940, 10: 35934}, + ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 20458}, + ("Category Fives", 4, 4): {0: 5362, 5: 23073, 10: 37379, 15: 34186}, + ("Category Fives", 4, 5): {0: 2655, 5: 15662, 10: 34602, 15: 34186, 20: 12895}, + ("Category Fives", 4, 6): {0: 2059, 10: 19678, 15: 48376, 20: 29887}, + ("Category Fives", 4, 7): {0: 1473, 15: 34402, 20: 64125}, + ("Category Fives", 4, 8): {0: 551, 20: 99449}, + ("Category Fives", 5, 0): {0: 100000}, + ("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, 5: 23716, 10: 34430, 15: 35328}, + ("Category Fives", 5, 4): {0: 2615, 5: 13975, 10: 30133, 15: 32247, 20: 21030}, + ("Category Fives", 5, 5): {0: 1482, 10: 13532, 15: 37597, 20: 47389}, + ("Category Fives", 5, 6): {0: 477, 15: 14484, 20: 71985, 25: 13054}, + ("Category Fives", 5, 7): {0: 273, 20: 52865, 25: 46862}, + ("Category Fives", 5, 8): {20: 16822, 25: 83178}, + ("Category Fives", 6, 0): {0: 100000}, + ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 26357}, + ("Category Fives", 6, 2): {0: 11322, 5: 29613, 10: 32664, 15: 26401}, + ("Category Fives", 6, 3): {0: 3765, 5: 16288, 10: 29770, 15: 29233, 20: 20944}, + ("Category Fives", 6, 4): {0: 1889, 10: 13525, 15: 33731, 20: 38179, 25: 12676}, + ("Category Fives", 6, 5): {0: 53, 10: 11118, 20: 47588, 25: 41241}, + ("Category Fives", 6, 6): {0: 10, 20: 8876, 25: 91114}, + ("Category Fives", 6, 7): {0: 7, 25: 85815, 30: 14178}, + ("Category Fives", 6, 8): {25: 43072, 30: 56928}, + ("Category Fives", 7, 0): {0: 100000}, + ("Category Fives", 7, 1): {0: 27826, 5: 39154, 10: 33020}, + ("Category Fives", 7, 2): {0: 7609, 5: 24193, 10: 31722, 15: 23214, 20: 13262}, + ("Category Fives", 7, 3): {0: 1879, 15: 23021, 20: 75100}, + ("Category Fives", 7, 4): {0: 345, 20: 64636, 25: 35019}, + ("Category Fives", 7, 5): {0: 40, 20: 7522, 25: 76792, 30: 15646}, + ("Category Fives", 7, 6): {0: 8, 25: 26517, 30: 73475}, + ("Category Fives", 7, 7): {0: 2, 30: 99998}, + ("Category Fives", 7, 8): {30: 84211, 35: 15789}, + ("Category Fives", 8, 0): {0: 100000}, + ("Category Fives", 8, 1): {0: 23333, 5: 37259, 10: 25947, 15: 13461}, + ("Category Fives", 8, 2): {0: 5425, 5: 18915, 10: 29380, 15: 25994, 20: 20286}, + ("Category Fives", 8, 3): {0: 495, 20: 78726, 25: 20779}, + ("Category Fives", 8, 4): {20: 12998, 25: 70085, 30: 16917}, + ("Category Fives", 8, 5): {25: 15859, 30: 84141}, + ("Category Fives", 8, 6): {30: 80722, 35: 19278}, + ("Category Fives", 8, 7): {30: 23955, 35: 76045}, + ("Category Fives", 8, 8): {35: 100000}, + ("Category Sixes", 0, 0): {0: 100000}, + ("Category Sixes", 0, 1): {0: 100000}, + ("Category Sixes", 0, 2): {0: 100000}, + ("Category Sixes", 0, 3): {0: 100000}, + ("Category Sixes", 0, 4): {0: 100000}, + ("Category Sixes", 0, 5): {0: 100000}, + ("Category Sixes", 0, 6): {0: 100000}, + ("Category Sixes", 0, 7): {0: 100000}, + ("Category Sixes", 0, 8): {0: 100000}, + ("Category Sixes", 1, 0): {0: 100000}, + ("Category Sixes", 1, 1): {0: 83168, 6: 16832}, + ("Category Sixes", 1, 2): {0: 69548, 6: 30452}, + ("Category Sixes", 1, 3): {0: 57697, 6: 42303}, + ("Category Sixes", 1, 4): {0: 48043, 6: 51957}, + ("Category Sixes", 1, 5): {0: 39912, 6: 60088}, + ("Category Sixes", 1, 6): {0: 33499, 6: 66501}, + ("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: 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}, + ("Category Sixes", 2, 6): {0: 11073, 6: 44833, 12: 44094}, + ("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: 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", 4, 0): {0: 100000}, + ("Category Sixes", 4, 1): {0: 48197, 6: 38521, 12: 13282}, + ("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, 6: 23265, 12: 37209, 18: 34202}, + ("Category Sixes", 4, 5): {0: 2658, 6: 15488, 12: 34685, 18: 34476, 24: 12693}, + ("Category Sixes", 4, 6): {0: 2045, 12: 19683, 18: 48559, 24: 29713}, + ("Category Sixes", 4, 7): {0: 1470, 18: 34646, 24: 63884}, + ("Category Sixes", 4, 8): {0: 22, 18: 12111, 24: 87867}, + ("Category Sixes", 5, 0): {0: 100000}, + ("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, 6: 23539, 12: 34585, 18: 25020, 24: 10400}, + ("Category Sixes", 5, 4): {0: 2581, 6: 13980, 12: 30355, 18: 32198, 24: 20886}, + ("Category Sixes", 5, 5): {0: 1472, 12: 13518, 18: 37752, 24: 47258}, + ("Category Sixes", 5, 6): {0: 476, 18: 14559, 24: 71856, 30: 13109}, + ("Category Sixes", 5, 7): {0: 275, 24: 52573, 30: 47152}, + ("Category Sixes", 5, 8): {24: 16500, 30: 83500}, + ("Category Sixes", 6, 0): {0: 100000}, + ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 12: 26466}, + ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 18: 26710}, + ("Category Sixes", 6, 3): {0: 3787, 6: 16266, 12: 29873, 18: 29107, 24: 20967}, + ("Category Sixes", 6, 4): {0: 1875, 12: 13602, 18: 33731, 24: 38090, 30: 12702}, + ("Category Sixes", 6, 5): {0: 433, 18: 10665, 24: 47398, 30: 41504}, + ("Category Sixes", 6, 6): {0: 89, 24: 14905, 30: 85006}, + ("Category Sixes", 6, 7): {0: 19, 30: 85816, 36: 14165}, + ("Category Sixes", 6, 8): {30: 43219, 36: 56781}, + ("Category Sixes", 7, 0): {0: 100000}, + ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 12: 33164}, + ("Category Sixes", 7, 2): {0: 7883, 6: 23846, 12: 31558, 18: 23295, 24: 13418}, + ("Category Sixes", 7, 3): {0: 2186, 6: 10928, 12: 24321, 18: 29650, 24: 21177, 30: 11738}, + ("Category Sixes", 7, 4): {0: 1034, 18: 12857, 24: 37227, 30: 48882}, + ("Category Sixes", 7, 5): {0: 300, 30: 83887, 36: 15813}, + ("Category Sixes", 7, 6): {30: 31359, 36: 68641}, + ("Category Sixes", 7, 7): {36: 89879, 42: 10121}, + ("Category Sixes", 7, 8): {36: 49549, 42: 50451}, + ("Category Sixes", 8, 0): {0: 100000}, + ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 13606}, + ("Category Sixes", 8, 2): {0: 5280, 6: 18943, 12: 29664, 18: 25777, 24: 20336}, + ("Category Sixes", 8, 3): {0: 2024, 12: 12586, 18: 28717, 24: 35860, 30: 20813}, + ("Category Sixes", 8, 4): {0: 175, 24: 10907, 30: 72017, 36: 16901}, + ("Category Sixes", 8, 5): {0: 1, 30: 23224, 36: 66215, 42: 10560}, + ("Category Sixes", 8, 6): {36: 29563, 42: 70437}, + ("Category Sixes", 8, 7): {42: 99990, 36: 10}, + ("Category Sixes", 8, 8): {42: 87843, 48: 12157}, + ("Category Choice", 0, 0): {0: 100000}, + ("Category Choice", 0, 1): {0: 100000}, + ("Category Choice", 0, 2): {0: 100000}, + ("Category Choice", 0, 3): {0: 100000}, + ("Category Choice", 0, 4): {0: 100000}, + ("Category Choice", 0, 5): {0: 100000}, + ("Category Choice", 0, 6): {0: 100000}, + ("Category Choice", 0, 7): {0: 100000}, + ("Category Choice", 0, 8): {0: 100000}, + ("Category Choice", 1, 0): {0: 100000}, + ("Category Choice", 1, 1): {1: 33315, 3: 66685}, + ("Category Choice", 1, 2): {1: 32981, 4: 67019}, + ("Category Choice", 1, 3): {1: 12312, 4: 25020, 5: 62668}, + ("Category Choice", 1, 4): {1: 11564, 5: 88436}, + ("Category Choice", 1, 5): {1: 2956, 5: 97044}, + ("Category Choice", 1, 6): {4: 1024, 6: 65357, 5: 33619}, + ("Category Choice", 1, 7): {6: 100000}, + ("Category Choice", 1, 8): {6: 100000}, + ("Category Choice", 2, 0): {0: 100000}, + ("Category Choice", 2, 1): {2: 27810, 6: 72190}, + ("Category Choice", 2, 2): {2: 10285, 6: 26698, 8: 63017}, + ("Category Choice", 2, 3): {2: 3965, 8: 96035}, + ("Category Choice", 2, 4): {2: 143, 8: 33731, 9: 66126}, + ("Category Choice", 2, 5): {8: 12687, 10: 62544, 9: 24769}, + ("Category Choice", 2, 6): {10: 100000}, + ("Category Choice", 2, 7): {11: 66194, 10: 33806}, + ("Category Choice", 2, 8): {11: 100000}, + ("Category Choice", 3, 0): {0: 100000}, + ("Category Choice", 3, 1): {3: 10461, 6: 27156, 10: 62383}, + ("Category Choice", 3, 2): {3: 3586, 6: 31281, 12: 65133}, + ("Category Choice", 3, 3): {3: 1491, 12: 33737, 13: 64772}, + ("Category Choice", 3, 4): {12: 13802, 14: 60820, 13: 25378}, + ("Category Choice", 3, 5): {14: 99999, 13: 1}, + ("Category Choice", 3, 6): {15: 64851, 14: 35149}, + ("Category Choice", 3, 7): {16: 62341, 15: 24422, 14: 13237}, + ("Category Choice", 3, 8): {16: 100000}, + ("Category Choice", 4, 0): {0: 100000}, + ("Category Choice", 4, 1): {4: 4748, 10: 28815, 13: 66437}, + ("Category Choice", 4, 2): {12: 12006, 16: 64226, 13: 23768}, + ("Category Choice", 4, 3): {16: 32567, 17: 67433}, + ("Category Choice", 4, 4): {16: 30845, 18: 69155}, + ("Category Choice", 4, 5): {16: 9568, 19: 68981, 18: 21451}, + ("Category Choice", 4, 6): {18: 10841, 20: 65051, 19: 24108}, + ("Category Choice", 4, 7): {18: 4198, 21: 61270, 20: 25195, 19: 9337}, + ("Category Choice", 4, 8): {21: 99999, 20: 1}, + ("Category Choice", 5, 0): {0: 100000}, + ("Category Choice", 5, 1): {5: 4485, 13: 35340, 17: 60175}, + ("Category Choice", 5, 2): {16: 35286, 20: 64714}, + ("Category Choice", 5, 3): {20: 39254, 22: 60746}, + ("Category Choice", 5, 4): {20: 35320, 23: 64680}, + ("Category Choice", 5, 5): {22: 33253, 24: 66747}, + ("Category Choice", 5, 6): {22: 11089, 25: 66653, 24: 22258}, + ("Category Choice", 5, 7): {24: 12216, 26: 63214, 22: 50, 25: 24520}, + ("Category Choice", 5, 8): {24: 4866, 27: 60314, 26: 25089, 25: 9731}, + ("Category Choice", 6, 0): {0: 100000}, + ("Category Choice", 6, 1): {6: 2276, 17: 33774, 20: 63950}, + ("Category Choice", 6, 2): {20: 34853, 24: 65147}, + ("Category Choice", 6, 3): {22: 12477, 26: 64201, 24: 23322}, + ("Category Choice", 6, 4): {24: 14073, 28: 60688, 26: 25239}, + ("Category Choice", 6, 5): {26: 35591, 29: 64409}, + ("Category Choice", 6, 6): {26: 33229, 30: 66771}, + ("Category Choice", 6, 7): {28: 33078, 31: 66922}, + ("Category Choice", 6, 8): {28: 12143, 31: 24567, 32: 63290}, + ("Category Choice", 7, 0): {0: 100000}, + ("Category Choice", 7, 1): {7: 1558, 20: 31716, 23: 66726}, + ("Category Choice", 7, 2): {23: 34663, 28: 65337}, + ("Category Choice", 7, 3): {28: 32932, 30: 67062, 23: 6}, + ("Category Choice", 7, 4): {28: 11163, 32: 66108, 30: 22729}, + ("Category Choice", 7, 5): {30: 12528, 34: 63034, 32: 24438}, + ("Category Choice", 7, 6): {30: 4270, 35: 65916, 34: 21485, 32: 8329}, + ("Category Choice", 7, 7): {32: 4014, 36: 68134, 35: 21006, 34: 6846}, + ("Category Choice", 7, 8): {34: 3434, 37: 68373, 36: 21550, 35: 6643}, + ("Category Choice", 8, 0): {0: 100000}, + ("Category Choice", 8, 1): {10: 1400, 23: 36664, 27: 61936}, + ("Category Choice", 8, 2): {27: 34595, 32: 65405}, + ("Category Choice", 8, 3): {30: 13097, 35: 62142, 32: 24761}, + ("Category Choice", 8, 4): {35: 36672, 37: 63328}, + ("Category Choice", 8, 5): {39: 61292, 35: 14195, 37: 24513}, + ("Category Choice", 8, 6): {40: 65672, 39: 21040, 35: 4873, 37: 8415}, + ("Category Choice", 8, 7): {39: 13561, 42: 60493, 40: 25946}, + ("Category Choice", 8, 8): {39: 5250, 43: 61284, 42: 23421, 40: 10045}, + ("Category Inverse Choice", 0, 0): {0: 100000}, + ("Category Inverse Choice", 0, 1): {0: 100000}, + ("Category Inverse Choice", 0, 2): {0: 100000}, + ("Category Inverse Choice", 0, 3): {0: 100000}, + ("Category Inverse Choice", 0, 4): {0: 100000}, + ("Category Inverse Choice", 0, 5): {0: 100000}, + ("Category Inverse Choice", 0, 6): {0: 100000}, + ("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: 33315, 3: 66685}, + ("Category Inverse Choice", 1, 2): {1: 32981, 4: 67019}, + ("Category Inverse Choice", 1, 3): {1: 12312, 4: 25020, 5: 62668}, + ("Category Inverse Choice", 1, 4): {1: 11564, 5: 88436}, + ("Category Inverse Choice", 1, 5): {1: 2956, 5: 97044}, + ("Category Inverse Choice", 1, 6): {4: 1024, 6: 65357, 5: 33619}, + ("Category Inverse Choice", 1, 7): {6: 100000}, + ("Category Inverse Choice", 1, 8): {6: 100000}, + ("Category Inverse Choice", 2, 0): {0: 100000}, + ("Category Inverse Choice", 2, 1): {2: 27810, 6: 72190}, + ("Category Inverse Choice", 2, 2): {2: 10285, 6: 26698, 8: 63017}, + ("Category Inverse Choice", 2, 3): {2: 3965, 8: 96035}, + ("Category Inverse Choice", 2, 4): {2: 143, 8: 33731, 9: 66126}, + ("Category Inverse Choice", 2, 5): {8: 12687, 10: 62544, 9: 24769}, + ("Category Inverse Choice", 2, 6): {10: 100000}, + ("Category Inverse Choice", 2, 7): {11: 66194, 10: 33806}, + ("Category Inverse Choice", 2, 8): {11: 100000}, + ("Category Inverse Choice", 3, 0): {0: 100000}, + ("Category Inverse Choice", 3, 1): {3: 10461, 6: 27156, 10: 62383}, + ("Category Inverse Choice", 3, 2): {3: 3586, 6: 31281, 12: 65133}, + ("Category Inverse Choice", 3, 3): {3: 1491, 12: 33737, 13: 64772}, + ("Category Inverse Choice", 3, 4): {12: 13802, 14: 60820, 13: 25378}, + ("Category Inverse Choice", 3, 5): {14: 99999, 13: 1}, + ("Category Inverse Choice", 3, 6): {15: 64851, 14: 35149}, + ("Category Inverse Choice", 3, 7): {16: 62341, 15: 24422, 14: 13237}, + ("Category Inverse Choice", 3, 8): {16: 100000}, + ("Category Inverse Choice", 4, 0): {0: 100000}, + ("Category Inverse Choice", 4, 1): {4: 4748, 10: 28815, 13: 66437}, + ("Category Inverse Choice", 4, 2): {12: 12006, 16: 64226, 13: 23768}, + ("Category Inverse Choice", 4, 3): {16: 32567, 17: 67433}, + ("Category Inverse Choice", 4, 4): {16: 30845, 18: 69155}, + ("Category Inverse Choice", 4, 5): {16: 9568, 19: 68981, 18: 21451}, + ("Category Inverse Choice", 4, 6): {18: 10841, 20: 65051, 19: 24108}, + ("Category Inverse Choice", 4, 7): {18: 4198, 21: 61270, 20: 25195, 19: 9337}, + ("Category Inverse Choice", 4, 8): {21: 99999, 20: 1}, + ("Category Inverse Choice", 5, 0): {0: 100000}, + ("Category Inverse Choice", 5, 1): {5: 4485, 13: 35340, 17: 60175}, + ("Category Inverse Choice", 5, 2): {16: 35286, 20: 64714}, + ("Category Inverse Choice", 5, 3): {20: 39254, 22: 60746}, + ("Category Inverse Choice", 5, 4): {20: 35320, 23: 64680}, + ("Category Inverse Choice", 5, 5): {22: 33253, 24: 66747}, + ("Category Inverse Choice", 5, 6): {22: 11089, 25: 66653, 24: 22258}, + ("Category Inverse Choice", 5, 7): {24: 12216, 26: 63214, 22: 50, 25: 24520}, + ("Category Inverse Choice", 5, 8): {24: 4866, 27: 60314, 26: 25089, 25: 9731}, + ("Category Inverse Choice", 6, 0): {0: 100000}, + ("Category Inverse Choice", 6, 1): {6: 2276, 17: 33774, 20: 63950}, + ("Category Inverse Choice", 6, 2): {20: 34853, 24: 65147}, + ("Category Inverse Choice", 6, 3): {22: 12477, 26: 64201, 24: 23322}, + ("Category Inverse Choice", 6, 4): {24: 14073, 28: 60688, 26: 25239}, + ("Category Inverse Choice", 6, 5): {26: 35591, 29: 64409}, + ("Category Inverse Choice", 6, 6): {26: 33229, 30: 66771}, + ("Category Inverse Choice", 6, 7): {28: 33078, 31: 66922}, + ("Category Inverse Choice", 6, 8): {28: 12143, 31: 24567, 32: 63290}, + ("Category Inverse Choice", 7, 0): {0: 100000}, + ("Category Inverse Choice", 7, 1): {7: 1558, 20: 31716, 23: 66726}, + ("Category Inverse Choice", 7, 2): {23: 34663, 28: 65337}, + ("Category Inverse Choice", 7, 3): {28: 32932, 30: 67062, 23: 6}, + ("Category Inverse Choice", 7, 4): {28: 11163, 32: 66108, 30: 22729}, + ("Category Inverse Choice", 7, 5): {30: 12528, 34: 63034, 32: 24438}, + ("Category Inverse Choice", 7, 6): {30: 4270, 35: 65916, 34: 21485, 32: 8329}, + ("Category Inverse Choice", 7, 7): {32: 4014, 36: 68134, 35: 21006, 34: 6846}, + ("Category Inverse Choice", 7, 8): {34: 3434, 37: 68373, 36: 21550, 35: 6643}, + ("Category Inverse Choice", 8, 0): {0: 100000}, + ("Category Inverse Choice", 8, 1): {10: 1400, 23: 36664, 27: 61936}, + ("Category Inverse Choice", 8, 2): {27: 34595, 32: 65405}, + ("Category Inverse Choice", 8, 3): {30: 13097, 35: 62142, 32: 24761}, + ("Category Inverse Choice", 8, 4): {35: 36672, 37: 63328}, + ("Category Inverse Choice", 8, 5): {39: 61292, 35: 14195, 37: 24513}, + ("Category Inverse Choice", 8, 6): {40: 65672, 39: 21040, 35: 4873, 37: 8415}, + ("Category Inverse Choice", 8, 7): {39: 13561, 42: 60493, 40: 25946}, + ("Category Inverse Choice", 8, 8): {39: 5250, 43: 61284, 42: 23421, 40: 10045}, + ("Category Pair", 0, 0): {0: 100000}, + ("Category Pair", 0, 1): {0: 100000}, + ("Category Pair", 0, 2): {0: 100000}, + ("Category Pair", 0, 3): {0: 100000}, + ("Category Pair", 0, 4): {0: 100000}, + ("Category Pair", 0, 5): {0: 100000}, + ("Category Pair", 0, 6): {0: 100000}, + ("Category Pair", 0, 7): {0: 100000}, + ("Category Pair", 0, 8): {0: 100000}, + ("Category Pair", 1, 0): {0: 100000}, + ("Category Pair", 1, 1): {0: 100000}, + ("Category Pair", 1, 2): {0: 100000}, + ("Category Pair", 1, 3): {0: 100000}, + ("Category Pair", 1, 4): {0: 100000}, + ("Category Pair", 1, 5): {0: 100000}, + ("Category Pair", 1, 6): {0: 100000}, + ("Category Pair", 1, 7): {0: 100000}, + ("Category Pair", 1, 8): {0: 100000}, + ("Category Pair", 2, 0): {0: 100000}, + ("Category Pair", 2, 1): {0: 83388, 10: 16612}, + ("Category Pair", 2, 2): {0: 69422, 10: 30578}, + ("Category Pair", 2, 3): {0: 57830, 10: 42170}, + ("Category Pair", 2, 4): {0: 48195, 10: 51805}, + ("Category Pair", 2, 5): {0: 40117, 10: 59883}, + ("Category Pair", 2, 6): {0: 33286, 10: 66714}, + ("Category Pair", 2, 7): {0: 27917, 10: 72083}, + ("Category Pair", 2, 8): {0: 23354, 10: 76646}, + ("Category Pair", 3, 0): {0: 100000}, + ("Category Pair", 3, 1): {0: 55518, 10: 44482}, + ("Category Pair", 3, 2): {0: 30904, 10: 69096}, + ("Category Pair", 3, 3): {0: 17242, 10: 82758}, + ("Category Pair", 3, 4): {0: 9486, 10: 90514}, + ("Category Pair", 3, 5): {0: 5362, 10: 94638}, + ("Category Pair", 3, 6): {0: 2909, 10: 97091}, + ("Category Pair", 3, 7): {0: 1574, 10: 98426}, + ("Category Pair", 3, 8): {0: 902, 10: 99098}, + ("Category Pair", 4, 0): {0: 100000}, + ("Category Pair", 4, 1): {0: 27789, 10: 72211}, + ("Category Pair", 4, 2): {0: 7799, 10: 92201}, + ("Category Pair", 4, 3): {0: 2113, 10: 97887}, + ("Category Pair", 4, 4): {0: 601, 10: 99399}, + ("Category Pair", 4, 5): {0: 155, 10: 99845}, + ("Category Pair", 4, 6): {0: 43, 10: 99957}, + ("Category Pair", 4, 7): {0: 10, 10: 99990}, + ("Category Pair", 4, 8): {0: 3, 10: 99997}, + ("Category Pair", 5, 0): {0: 100000}, + ("Category Pair", 5, 1): {0: 9298, 10: 90702}, + ("Category Pair", 5, 2): {0: 863, 10: 99137}, + ("Category Pair", 5, 3): {0: 79, 10: 99921}, + ("Category Pair", 5, 4): {0: 2, 10: 99998}, + ("Category Pair", 5, 5): {0: 2, 10: 99998}, + ("Category Pair", 5, 6): {10: 100000}, + ("Category Pair", 5, 7): {10: 100000}, + ("Category Pair", 5, 8): {10: 100000}, + ("Category Pair", 6, 0): {0: 100000}, + ("Category Pair", 6, 1): {0: 1541, 10: 98459}, + ("Category Pair", 6, 2): {0: 23, 10: 99977}, + ("Category Pair", 6, 3): {10: 100000}, + ("Category Pair", 6, 4): {10: 100000}, + ("Category Pair", 6, 5): {10: 100000}, + ("Category Pair", 6, 6): {10: 100000}, + ("Category Pair", 6, 7): {10: 100000}, + ("Category Pair", 6, 8): {10: 100000}, + ("Category Pair", 7, 0): {0: 100000}, + ("Category Pair", 7, 1): {10: 100000}, + ("Category Pair", 7, 2): {10: 100000}, + ("Category Pair", 7, 3): {10: 100000}, + ("Category Pair", 7, 4): {10: 100000}, + ("Category Pair", 7, 5): {10: 100000}, + ("Category Pair", 7, 6): {10: 100000}, + ("Category Pair", 7, 7): {10: 100000}, + ("Category Pair", 7, 8): {10: 100000}, + ("Category Pair", 8, 0): {0: 100000}, + ("Category Pair", 8, 1): {10: 100000}, + ("Category Pair", 8, 2): {10: 100000}, + ("Category Pair", 8, 3): {10: 100000}, + ("Category Pair", 8, 4): {10: 100000}, + ("Category Pair", 8, 5): {10: 100000}, + ("Category Pair", 8, 6): {10: 100000}, + ("Category Pair", 8, 7): {10: 100000}, + ("Category Pair", 8, 8): {10: 100000}, + ("Category Three of a Kind", 0, 0): {0: 100000}, + ("Category Three of a Kind", 0, 1): {0: 100000}, + ("Category Three of a Kind", 0, 2): {0: 100000}, + ("Category Three of a Kind", 0, 3): {0: 100000}, + ("Category Three of a Kind", 0, 4): {0: 100000}, + ("Category Three of a Kind", 0, 5): {0: 100000}, + ("Category Three of a Kind", 0, 6): {0: 100000}, + ("Category Three of a Kind", 0, 7): {0: 100000}, + ("Category Three of a Kind", 0, 8): {0: 100000}, + ("Category Three of a Kind", 1, 0): {0: 100000}, + ("Category Three of a Kind", 1, 1): {0: 100000}, + ("Category Three of a Kind", 1, 2): {0: 100000}, + ("Category Three of a Kind", 1, 3): {0: 100000}, + ("Category Three of a Kind", 1, 4): {0: 100000}, + ("Category Three of a Kind", 1, 5): {0: 100000}, + ("Category Three of a Kind", 1, 6): {0: 100000}, + ("Category Three of a Kind", 1, 7): {0: 100000}, + ("Category Three of a Kind", 1, 8): {0: 100000}, + ("Category Three of a Kind", 2, 0): {0: 100000}, + ("Category Three of a Kind", 2, 1): {0: 100000}, + ("Category Three of a Kind", 2, 2): {0: 100000}, + ("Category Three of a Kind", 2, 3): {0: 100000}, + ("Category Three of a Kind", 2, 4): {0: 100000}, + ("Category Three of a Kind", 2, 5): {0: 100000}, + ("Category Three of a Kind", 2, 6): {0: 100000}, + ("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: 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}, + ("Category Three of a Kind", 3, 5): {0: 57476, 20: 42524}, + ("Category Three of a Kind", 3, 6): {0: 48510, 20: 51490}, + ("Category Three of a Kind", 3, 7): {0: 40921, 20: 59079}, + ("Category Three of a Kind", 3, 8): {0: 34533, 20: 65467}, + ("Category Three of a Kind", 4, 0): {0: 100000}, + ("Category Three of a Kind", 4, 1): {0: 90316, 20: 9684}, + ("Category Three of a Kind", 4, 2): {0: 68401, 20: 31599}, + ("Category Three of a Kind", 4, 3): {0: 49383, 20: 50617}, + ("Category Three of a Kind", 4, 4): {0: 34399, 20: 65601}, + ("Category Three of a Kind", 4, 5): {0: 24154, 20: 75846}, + ("Category Three of a Kind", 4, 6): {0: 16802, 20: 83198}, + ("Category Three of a Kind", 4, 7): {0: 11623, 20: 88377}, + ("Category Three of a Kind", 4, 8): {0: 8105, 20: 91895}, + ("Category Three of a Kind", 5, 0): {0: 100000}, + ("Category Three of a Kind", 5, 1): {0: 78629, 20: 21371}, + ("Category Three of a Kind", 5, 2): {0: 46013, 20: 53987}, + ("Category Three of a Kind", 5, 3): {0: 25698, 20: 74302}, + ("Category Three of a Kind", 5, 4): {0: 14205, 20: 85795}, + ("Category Three of a Kind", 5, 5): {0: 7932, 20: 92068}, + ("Category Three of a Kind", 5, 6): {0: 4357, 20: 95643}, + ("Category Three of a Kind", 5, 7): {0: 2432, 20: 97568}, + ("Category Three of a Kind", 5, 8): {0: 1378, 20: 98622}, + ("Category Three of a Kind", 6, 0): {0: 100000}, + ("Category Three of a Kind", 6, 1): {0: 63231, 20: 36769}, + ("Category Three of a Kind", 6, 2): {0: 26818, 20: 73182}, + ("Category Three of a Kind", 6, 3): {0: 11075, 20: 88925}, + ("Category Three of a Kind", 6, 4): {0: 4749, 20: 95251}, + ("Category Three of a Kind", 6, 5): {0: 1982, 20: 98018}, + ("Category Three of a Kind", 6, 6): {0: 827, 20: 99173}, + ("Category Three of a Kind", 6, 7): {0: 358, 20: 99642}, + ("Category Three of a Kind", 6, 8): {0: 146, 20: 99854}, + ("Category Three of a Kind", 7, 0): {0: 100000}, + ("Category Three of a Kind", 7, 1): {0: 45975, 20: 54025}, + ("Category Three of a Kind", 7, 2): {0: 13207, 20: 86793}, + ("Category Three of a Kind", 7, 3): {0: 3727, 20: 96273}, + ("Category Three of a Kind", 7, 4): {0: 1097, 20: 98903}, + ("Category Three of a Kind", 7, 5): {0: 313, 20: 99687}, + ("Category Three of a Kind", 7, 6): {0: 96, 20: 99904}, + ("Category Three of a Kind", 7, 7): {0: 22, 20: 99978}, + ("Category Three of a Kind", 7, 8): {0: 8, 20: 99992}, + ("Category Three of a Kind", 8, 0): {0: 100000}, + ("Category Three of a Kind", 8, 1): {0: 29316, 20: 70684}, + ("Category Three of a Kind", 8, 2): {0: 5027, 20: 94973}, + ("Category Three of a Kind", 8, 3): {0: 857, 20: 99143}, + ("Category Three of a Kind", 8, 4): {0: 162, 20: 99838}, + ("Category Three of a Kind", 8, 5): {0: 25, 20: 99975}, + ("Category Three of a Kind", 8, 6): {0: 4, 20: 99996}, + ("Category Three of a Kind", 8, 7): {0: 1, 20: 99999}, + ("Category Three of a Kind", 8, 8): {20: 100000}, + ("Category Four of a Kind", 0, 0): {0: 100000}, + ("Category Four of a Kind", 0, 1): {0: 100000}, + ("Category Four of a Kind", 0, 2): {0: 100000}, + ("Category Four of a Kind", 0, 3): {0: 100000}, + ("Category Four of a Kind", 0, 4): {0: 100000}, + ("Category Four of a Kind", 0, 5): {0: 100000}, + ("Category Four of a Kind", 0, 6): {0: 100000}, + ("Category Four of a Kind", 0, 7): {0: 100000}, + ("Category Four of a Kind", 0, 8): {0: 100000}, + ("Category Four of a Kind", 1, 0): {0: 100000}, + ("Category Four of a Kind", 1, 1): {0: 100000}, + ("Category Four of a Kind", 1, 2): {0: 100000}, + ("Category Four of a Kind", 1, 3): {0: 100000}, + ("Category Four of a Kind", 1, 4): {0: 100000}, + ("Category Four of a Kind", 1, 5): {0: 100000}, + ("Category Four of a Kind", 1, 6): {0: 100000}, + ("Category Four of a Kind", 1, 7): {0: 100000}, + ("Category Four of a Kind", 1, 8): {0: 100000}, + ("Category Four of a Kind", 2, 0): {0: 100000}, + ("Category Four of a Kind", 2, 1): {0: 100000}, + ("Category Four of a Kind", 2, 2): {0: 100000}, + ("Category Four of a Kind", 2, 3): {0: 100000}, + ("Category Four of a Kind", 2, 4): {0: 100000}, + ("Category Four of a Kind", 2, 5): {0: 100000}, + ("Category Four of a Kind", 2, 6): {0: 100000}, + ("Category Four of a Kind", 2, 7): {0: 100000}, + ("Category Four of a Kind", 2, 8): {0: 100000}, + ("Category Four of a Kind", 3, 0): {0: 100000}, + ("Category Four of a Kind", 3, 1): {0: 100000}, + ("Category Four of a Kind", 3, 2): {0: 100000}, + ("Category Four of a Kind", 3, 3): {0: 100000}, + ("Category Four of a Kind", 3, 4): {0: 100000}, + ("Category Four of a Kind", 3, 5): {0: 100000}, + ("Category Four of a Kind", 3, 6): {0: 100000}, + ("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: 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}, + ("Category Four of a Kind", 4, 5): {0: 72893, 30: 27107}, + ("Category Four of a Kind", 4, 6): {0: 64000, 30: 36000}, + ("Category Four of a Kind", 4, 7): {0: 55921, 30: 44079}, + ("Category Four of a Kind", 4, 8): {0: 48175, 30: 51825}, + ("Category Four of a Kind", 5, 0): {0: 100000}, + ("Category Four of a Kind", 5, 1): {0: 97938, 30: 2062}, + ("Category Four of a Kind", 5, 2): {0: 86751, 30: 13249}, + ("Category Four of a Kind", 5, 3): {0: 70886, 30: 29114}, + ("Category Four of a Kind", 5, 4): {0: 54807, 30: 45193}, + ("Category Four of a Kind", 5, 5): {0: 41729, 30: 58271}, + ("Category Four of a Kind", 5, 6): {0: 30960, 30: 69040}, + ("Category Four of a Kind", 5, 7): {0: 22207, 30: 77793}, + ("Category Four of a Kind", 5, 8): {0: 16027, 30: 83973}, + ("Category Four of a Kind", 6, 0): {0: 100000}, + ("Category Four of a Kind", 6, 1): {0: 94810, 30: 5190}, + ("Category Four of a Kind", 6, 2): {0: 73147, 30: 26853}, + ("Category Four of a Kind", 6, 3): {0: 49873, 30: 50127}, + ("Category Four of a Kind", 6, 4): {0: 31913, 30: 68087}, + ("Category Four of a Kind", 6, 5): {0: 19877, 30: 80123}, + ("Category Four of a Kind", 6, 6): {0: 11973, 30: 88027}, + ("Category Four of a Kind", 6, 7): {0: 7324, 30: 92676}, + ("Category Four of a Kind", 6, 8): {0: 4221, 30: 95779}, + ("Category Four of a Kind", 7, 0): {0: 100000}, + ("Category Four of a Kind", 7, 1): {0: 89422, 30: 10578}, + ("Category Four of a Kind", 7, 2): {0: 57049, 30: 42951}, + ("Category Four of a Kind", 7, 3): {0: 30903, 30: 69097}, + ("Category Four of a Kind", 7, 4): {0: 15962, 30: 84038}, + ("Category Four of a Kind", 7, 5): {0: 8148, 30: 91852}, + ("Category Four of a Kind", 7, 6): {0: 3943, 30: 96057}, + ("Category Four of a Kind", 7, 7): {0: 1933, 30: 98067}, + ("Category Four of a Kind", 7, 8): {0: 912, 30: 99088}, + ("Category Four of a Kind", 8, 0): {0: 100000}, + ("Category Four of a Kind", 8, 1): {0: 81614, 30: 18386}, + ("Category Four of a Kind", 8, 2): {0: 40524, 30: 59476}, + ("Category Four of a Kind", 8, 3): {0: 17426, 30: 82574}, + ("Category Four of a Kind", 8, 4): {0: 6958, 30: 93042}, + ("Category Four of a Kind", 8, 5): {0: 2862, 30: 97138}, + ("Category Four of a Kind", 8, 6): {0: 1049, 30: 98951}, + ("Category Four of a Kind", 8, 7): {0: 401, 30: 99599}, + ("Category Four of a Kind", 8, 8): {0: 156, 30: 99844}, + ("Category Tiny Straight", 0, 0): {0: 100000}, + ("Category Tiny Straight", 0, 1): {0: 100000}, + ("Category Tiny Straight", 0, 2): {0: 100000}, + ("Category Tiny Straight", 0, 3): {0: 100000}, + ("Category Tiny Straight", 0, 4): {0: 100000}, + ("Category Tiny Straight", 0, 5): {0: 100000}, + ("Category Tiny Straight", 0, 6): {0: 100000}, + ("Category Tiny Straight", 0, 7): {0: 100000}, + ("Category Tiny Straight", 0, 8): {0: 100000}, + ("Category Tiny Straight", 1, 0): {0: 100000}, + ("Category Tiny Straight", 1, 1): {0: 100000}, + ("Category Tiny Straight", 1, 2): {0: 100000}, + ("Category Tiny Straight", 1, 3): {0: 100000}, + ("Category Tiny Straight", 1, 4): {0: 100000}, + ("Category Tiny Straight", 1, 5): {0: 100000}, + ("Category Tiny Straight", 1, 6): {0: 100000}, + ("Category Tiny Straight", 1, 7): {0: 100000}, + ("Category Tiny Straight", 1, 8): {0: 100000}, + ("Category Tiny Straight", 2, 0): {0: 100000}, + ("Category Tiny Straight", 2, 1): {0: 100000}, + ("Category Tiny Straight", 2, 2): {0: 100000}, + ("Category Tiny Straight", 2, 3): {0: 100000}, + ("Category Tiny Straight", 2, 4): {0: 100000}, + ("Category Tiny Straight", 2, 5): {0: 100000}, + ("Category Tiny Straight", 2, 6): {0: 100000}, + ("Category Tiny Straight", 2, 7): {0: 100000}, + ("Category Tiny Straight", 2, 8): {0: 100000}, + ("Category Tiny Straight", 3, 0): {0: 100000}, + ("Category Tiny Straight", 3, 1): {0: 91672, 20: 8328}, + ("Category Tiny Straight", 3, 2): {0: 79082, 20: 20918}, + ("Category Tiny Straight", 3, 3): {0: 66490, 20: 33510}, + ("Category Tiny Straight", 3, 4): {0: 55797, 20: 44203}, + ("Category Tiny Straight", 3, 5): {0: 46967, 20: 53033}, + ("Category Tiny Straight", 3, 6): {0: 39595, 20: 60405}, + ("Category Tiny Straight", 3, 7): {0: 33384, 20: 66616}, + ("Category Tiny Straight", 3, 8): {0: 28747, 20: 71253}, + ("Category Tiny Straight", 4, 0): {0: 100000}, + ("Category Tiny Straight", 4, 1): {0: 78812, 20: 21188}, + ("Category Tiny Straight", 4, 2): {0: 55525, 20: 44475}, + ("Category Tiny Straight", 4, 3): {0: 38148, 20: 61852}, + ("Category Tiny Straight", 4, 4): {0: 26432, 20: 73568}, + ("Category Tiny Straight", 4, 5): {0: 18225, 20: 81775}, + ("Category Tiny Straight", 4, 6): {0: 12758, 20: 87242}, + ("Category Tiny Straight", 4, 7): {0: 8991, 20: 91009}, + ("Category Tiny Straight", 4, 8): {0: 6325, 20: 93675}, + ("Category Tiny Straight", 5, 0): {0: 100000}, + ("Category Tiny Straight", 5, 1): {0: 64979, 20: 35021}, + ("Category Tiny Straight", 5, 2): {0: 36509, 20: 63491}, + ("Category Tiny Straight", 5, 3): {0: 20576, 20: 79424}, + ("Category Tiny Straight", 5, 4): {0: 11585, 20: 88415}, + ("Category Tiny Straight", 5, 5): {0: 6874, 20: 93126}, + ("Category Tiny Straight", 5, 6): {0: 3798, 20: 96202}, + ("Category Tiny Straight", 5, 7): {0: 2214, 20: 97786}, + ("Category Tiny Straight", 5, 8): {0: 1272, 20: 98728}, + ("Category Tiny Straight", 6, 0): {0: 100000}, + ("Category Tiny Straight", 6, 1): {0: 52157, 20: 47843}, + ("Category Tiny Straight", 6, 2): {0: 23641, 20: 76359}, + ("Category Tiny Straight", 6, 3): {0: 10883, 20: 89117}, + ("Category Tiny Straight", 6, 4): {0: 5127, 20: 94873}, + ("Category Tiny Straight", 6, 5): {0: 2442, 20: 97558}, + ("Category Tiny Straight", 6, 6): {0: 1158, 20: 98842}, + ("Category Tiny Straight", 6, 7): {0: 542, 20: 99458}, + ("Category Tiny Straight", 6, 8): {0: 252, 20: 99748}, + ("Category Tiny Straight", 7, 0): {0: 100000}, + ("Category Tiny Straight", 7, 1): {0: 41492, 20: 58508}, + ("Category Tiny Straight", 7, 2): {0: 15072, 20: 84928}, + ("Category Tiny Straight", 7, 3): {0: 5905, 20: 94095}, + ("Category Tiny Straight", 7, 4): {0: 2246, 20: 97754}, + ("Category Tiny Straight", 7, 5): {0: 942, 20: 99058}, + ("Category Tiny Straight", 7, 6): {0: 337, 20: 99663}, + ("Category Tiny Straight", 7, 7): {0: 155, 20: 99845}, + ("Category Tiny Straight", 7, 8): {0: 61, 20: 99939}, + ("Category Tiny Straight", 8, 0): {0: 100000}, + ("Category Tiny Straight", 8, 1): {0: 32993, 20: 67007}, + ("Category Tiny Straight", 8, 2): {0: 10074, 20: 89926}, + ("Category Tiny Straight", 8, 3): {0: 3158, 20: 96842}, + ("Category Tiny Straight", 8, 4): {0: 1060, 20: 98940}, + ("Category Tiny Straight", 8, 5): {0: 356, 20: 99644}, + ("Category Tiny Straight", 8, 6): {0: 117, 20: 99883}, + ("Category Tiny Straight", 8, 7): {0: 32, 20: 99968}, + ("Category Tiny Straight", 8, 8): {0: 10, 20: 99990}, + ("Category Small Straight", 0, 0): {0: 100000}, + ("Category Small Straight", 0, 1): {0: 100000}, + ("Category Small Straight", 0, 2): {0: 100000}, + ("Category Small Straight", 0, 3): {0: 100000}, + ("Category Small Straight", 0, 4): {0: 100000}, + ("Category Small Straight", 0, 5): {0: 100000}, + ("Category Small Straight", 0, 6): {0: 100000}, + ("Category Small Straight", 0, 7): {0: 100000}, + ("Category Small Straight", 0, 8): {0: 100000}, + ("Category Small Straight", 1, 0): {0: 100000}, + ("Category Small Straight", 1, 1): {0: 100000}, + ("Category Small Straight", 1, 2): {0: 100000}, + ("Category Small Straight", 1, 3): {0: 100000}, + ("Category Small Straight", 1, 4): {0: 100000}, + ("Category Small Straight", 1, 5): {0: 100000}, + ("Category Small Straight", 1, 6): {0: 100000}, + ("Category Small Straight", 1, 7): {0: 100000}, + ("Category Small Straight", 1, 8): {0: 100000}, + ("Category Small Straight", 2, 0): {0: 100000}, + ("Category Small Straight", 2, 1): {0: 100000}, + ("Category Small Straight", 2, 2): {0: 100000}, + ("Category Small Straight", 2, 3): {0: 100000}, + ("Category Small Straight", 2, 4): {0: 100000}, + ("Category Small Straight", 2, 5): {0: 100000}, + ("Category Small Straight", 2, 6): {0: 100000}, + ("Category Small Straight", 2, 7): {0: 100000}, + ("Category Small Straight", 2, 8): {0: 100000}, + ("Category Small Straight", 3, 0): {0: 100000}, + ("Category Small Straight", 3, 1): {0: 100000}, + ("Category Small Straight", 3, 2): {0: 100000}, + ("Category Small Straight", 3, 3): {0: 100000}, + ("Category Small Straight", 3, 4): {0: 100000}, + ("Category Small Straight", 3, 5): {0: 100000}, + ("Category Small Straight", 3, 6): {0: 100000}, + ("Category Small Straight", 3, 7): {0: 100000}, + ("Category Small Straight", 3, 8): {0: 100000}, + ("Category Small Straight", 4, 0): {0: 100000}, + ("Category Small Straight", 4, 1): {0: 94516, 30: 5484}, + ("Category Small Straight", 4, 2): {0: 82700, 30: 17300}, + ("Category Small Straight", 4, 3): {0: 67926, 30: 32074}, + ("Category Small Straight", 4, 4): {0: 54265, 30: 45735}, + ("Category Small Straight", 4, 5): {0: 42130, 30: 57870}, + ("Category Small Straight", 4, 6): {0: 32536, 30: 67464}, + ("Category Small Straight", 4, 7): {0: 25008, 30: 74992}, + ("Category Small Straight", 4, 8): {0: 19595, 30: 80405}, + ("Category Small Straight", 5, 0): {0: 100000}, + ("Category Small Straight", 5, 1): {0: 84528, 30: 15472}, + ("Category Small Straight", 5, 2): {0: 60775, 30: 39225}, + ("Category Small Straight", 5, 3): {0: 39543, 30: 60457}, + ("Category Small Straight", 5, 4): {0: 24760, 30: 75240}, + ("Category Small Straight", 5, 5): {0: 15713, 30: 84287}, + ("Category Small Straight", 5, 6): {0: 10199, 30: 89801}, + ("Category Small Straight", 5, 7): {0: 6618, 30: 93382}, + ("Category Small Straight", 5, 8): {0: 4205, 30: 95795}, + ("Category Small Straight", 6, 0): {0: 100000}, + ("Category Small Straight", 6, 1): {0: 73121, 30: 26879}, + ("Category Small Straight", 6, 2): {0: 41832, 30: 58168}, + ("Category Small Straight", 6, 3): {0: 21949, 30: 78051}, + ("Category Small Straight", 6, 4): {0: 11304, 30: 88696}, + ("Category Small Straight", 6, 5): {0: 6063, 30: 93937}, + ("Category Small Straight", 6, 6): {0: 3362, 30: 96638}, + ("Category Small Straight", 6, 7): {0: 1799, 30: 98201}, + ("Category Small Straight", 6, 8): {0: 1069, 30: 98931}, + ("Category Small Straight", 7, 0): {0: 100000}, + ("Category Small Straight", 7, 1): {0: 61837, 30: 38163}, + ("Category Small Straight", 7, 2): {0: 28202, 30: 71798}, + ("Category Small Straight", 7, 3): {0: 12187, 30: 87813}, + ("Category Small Straight", 7, 4): {0: 5427, 30: 94573}, + ("Category Small Straight", 7, 5): {0: 2444, 30: 97556}, + ("Category Small Straight", 7, 6): {0: 1144, 30: 98856}, + ("Category Small Straight", 7, 7): {0: 588, 30: 99412}, + ("Category Small Straight", 7, 8): {0: 258, 30: 99742}, + ("Category Small Straight", 8, 0): {0: 100000}, + ("Category Small Straight", 8, 1): {0: 51394, 30: 48606}, + ("Category Small Straight", 8, 2): {0: 19090, 30: 80910}, + ("Category Small Straight", 8, 3): {0: 7104, 30: 92896}, + ("Category Small Straight", 8, 4): {0: 2645, 30: 97355}, + ("Category Small Straight", 8, 5): {0: 1010, 30: 98990}, + ("Category Small Straight", 8, 6): {0: 408, 30: 99592}, + ("Category Small Straight", 8, 7): {0: 153, 30: 99847}, + ("Category Small Straight", 8, 8): {0: 78, 30: 99922}, + ("Category Large Straight", 0, 0): {0: 100000}, + ("Category Large Straight", 0, 1): {0: 100000}, + ("Category Large Straight", 0, 2): {0: 100000}, + ("Category Large Straight", 0, 3): {0: 100000}, + ("Category Large Straight", 0, 4): {0: 100000}, + ("Category Large Straight", 0, 5): {0: 100000}, + ("Category Large Straight", 0, 6): {0: 100000}, + ("Category Large Straight", 0, 7): {0: 100000}, + ("Category Large Straight", 0, 8): {0: 100000}, + ("Category Large Straight", 1, 0): {0: 100000}, + ("Category Large Straight", 1, 1): {0: 100000}, + ("Category Large Straight", 1, 2): {0: 100000}, + ("Category Large Straight", 1, 3): {0: 100000}, + ("Category Large Straight", 1, 4): {0: 100000}, + ("Category Large Straight", 1, 5): {0: 100000}, + ("Category Large Straight", 1, 6): {0: 100000}, + ("Category Large Straight", 1, 7): {0: 100000}, + ("Category Large Straight", 1, 8): {0: 100000}, + ("Category Large Straight", 2, 0): {0: 100000}, + ("Category Large Straight", 2, 1): {0: 100000}, + ("Category Large Straight", 2, 2): {0: 100000}, + ("Category Large Straight", 2, 3): {0: 100000}, + ("Category Large Straight", 2, 4): {0: 100000}, + ("Category Large Straight", 2, 5): {0: 100000}, + ("Category Large Straight", 2, 6): {0: 100000}, + ("Category Large Straight", 2, 7): {0: 100000}, + ("Category Large Straight", 2, 8): {0: 100000}, + ("Category Large Straight", 3, 0): {0: 100000}, + ("Category Large Straight", 3, 1): {0: 100000}, + ("Category Large Straight", 3, 2): {0: 100000}, + ("Category Large Straight", 3, 3): {0: 100000}, + ("Category Large Straight", 3, 4): {0: 100000}, + ("Category Large Straight", 3, 5): {0: 100000}, + ("Category Large Straight", 3, 6): {0: 100000}, + ("Category Large Straight", 3, 7): {0: 100000}, + ("Category Large Straight", 3, 8): {0: 100000}, + ("Category Large Straight", 4, 0): {0: 100000}, + ("Category Large Straight", 4, 1): {0: 100000}, + ("Category Large Straight", 4, 2): {0: 100000}, + ("Category Large Straight", 4, 3): {0: 100000}, + ("Category Large Straight", 4, 4): {0: 100000}, + ("Category Large Straight", 4, 5): {0: 100000}, + ("Category Large Straight", 4, 6): {0: 100000}, + ("Category Large Straight", 4, 7): {0: 100000}, + ("Category Large Straight", 4, 8): {0: 100000}, + ("Category Large Straight", 5, 0): {0: 100000}, + ("Category Large Straight", 5, 1): {0: 96929, 40: 3071}, + ("Category Large Straight", 5, 2): {0: 87056, 40: 12944}, + ("Category Large Straight", 5, 3): {0: 75101, 40: 24899}, + ("Category Large Straight", 5, 4): {0: 63617, 40: 36383}, + ("Category Large Straight", 5, 5): {0: 53149, 40: 46851}, + ("Category Large Straight", 5, 6): {0: 44321, 40: 55679}, + ("Category Large Straight", 5, 7): {0: 36948, 40: 63052}, + ("Category Large Straight", 5, 8): {0: 30661, 40: 69339}, + ("Category Large Straight", 6, 0): {0: 100000}, + ("Category Large Straight", 6, 1): {0: 90756, 40: 9244}, + ("Category Large Straight", 6, 2): {0: 69805, 40: 30195}, + ("Category Large Straight", 6, 3): {0: 49814, 40: 50186}, + ("Category Large Straight", 6, 4): {0: 35102, 40: 64898}, + ("Category Large Straight", 6, 5): {0: 24385, 40: 75615}, + ("Category Large Straight", 6, 6): {0: 17018, 40: 82982}, + ("Category Large Straight", 6, 7): {0: 11739, 40: 88261}, + ("Category Large Straight", 6, 8): {0: 7972, 40: 92028}, + ("Category Large Straight", 7, 0): {0: 100000}, + ("Category Large Straight", 7, 1): {0: 82840, 40: 17160}, + ("Category Large Straight", 7, 2): {0: 52821, 40: 47179}, + ("Category Large Straight", 7, 3): {0: 31348, 40: 68652}, + ("Category Large Straight", 7, 4): {0: 18166, 40: 81834}, + ("Category Large Straight", 7, 5): {0: 10690, 40: 89310}, + ("Category Large Straight", 7, 6): {0: 6051, 40: 93949}, + ("Category Large Straight", 7, 7): {0: 3617, 40: 96383}, + ("Category Large Straight", 7, 8): {0: 1941, 40: 98059}, + ("Category Large Straight", 8, 0): {0: 100000}, + ("Category Large Straight", 8, 1): {0: 73520, 40: 26480}, + ("Category Large Straight", 8, 2): {0: 39031, 40: 60969}, + ("Category Large Straight", 8, 3): {0: 19156, 40: 80844}, + ("Category Large Straight", 8, 4): {0: 9304, 40: 90696}, + ("Category Large Straight", 8, 5): {0: 4420, 40: 95580}, + ("Category Large Straight", 8, 6): {0: 2141, 40: 97859}, + ("Category Large Straight", 8, 7): {0: 1037, 40: 98963}, + ("Category Large Straight", 8, 8): {0: 511, 40: 99489}, + ("Category Full House", 0, 0): {0: 100000}, + ("Category Full House", 0, 1): {0: 100000}, + ("Category Full House", 0, 2): {0: 100000}, + ("Category Full House", 0, 3): {0: 100000}, + ("Category Full House", 0, 4): {0: 100000}, + ("Category Full House", 0, 5): {0: 100000}, + ("Category Full House", 0, 6): {0: 100000}, + ("Category Full House", 0, 7): {0: 100000}, + ("Category Full House", 0, 8): {0: 100000}, + ("Category Full House", 1, 0): {0: 100000}, + ("Category Full House", 1, 1): {0: 100000}, + ("Category Full House", 1, 2): {0: 100000}, + ("Category Full House", 1, 3): {0: 100000}, + ("Category Full House", 1, 4): {0: 100000}, + ("Category Full House", 1, 5): {0: 100000}, + ("Category Full House", 1, 6): {0: 100000}, + ("Category Full House", 1, 7): {0: 100000}, + ("Category Full House", 1, 8): {0: 100000}, + ("Category Full House", 2, 0): {0: 100000}, + ("Category Full House", 2, 1): {0: 100000}, + ("Category Full House", 2, 2): {0: 100000}, + ("Category Full House", 2, 3): {0: 100000}, + ("Category Full House", 2, 4): {0: 100000}, + ("Category Full House", 2, 5): {0: 100000}, + ("Category Full House", 2, 6): {0: 100000}, + ("Category Full House", 2, 7): {0: 100000}, + ("Category Full House", 2, 8): {0: 100000}, + ("Category Full House", 3, 0): {0: 100000}, + ("Category Full House", 3, 1): {0: 100000}, + ("Category Full House", 3, 2): {0: 100000}, + ("Category Full House", 3, 3): {0: 100000}, + ("Category Full House", 3, 4): {0: 100000}, + ("Category Full House", 3, 5): {0: 100000}, + ("Category Full House", 3, 6): {0: 100000}, + ("Category Full House", 3, 7): {0: 100000}, + ("Category Full House", 3, 8): {0: 100000}, + ("Category Full House", 4, 0): {0: 100000}, + ("Category Full House", 4, 1): {0: 100000}, + ("Category Full House", 4, 2): {0: 100000}, + ("Category Full House", 4, 3): {0: 100000}, + ("Category Full House", 4, 4): {0: 100000}, + ("Category Full House", 4, 5): {0: 100000}, + ("Category Full House", 4, 6): {0: 100000}, + ("Category Full House", 4, 7): {0: 100000}, + ("Category Full House", 4, 8): {0: 100000}, + ("Category Full House", 5, 0): {0: 100000}, + ("Category Full House", 5, 1): {0: 96155, 25: 3845}, + ("Category Full House", 5, 2): {0: 81391, 25: 18609}, + ("Category Full House", 5, 3): {0: 64300, 25: 35700}, + ("Category Full House", 5, 4): {0: 49669, 25: 50331}, + ("Category Full House", 5, 5): {0: 38019, 25: 61981}, + ("Category Full House", 5, 6): {0: 29751, 25: 70249}, + ("Category Full House", 5, 7): {0: 22960, 25: 77040}, + ("Category Full House", 5, 8): {0: 18650, 25: 81350}, + ("Category Full House", 6, 0): {0: 100000}, + ("Category Full House", 6, 1): {0: 82989, 25: 17011}, + ("Category Full House", 6, 2): {0: 47153, 25: 52847}, + ("Category Full House", 6, 3): {0: 24151, 25: 75849}, + ("Category Full House", 6, 4): {0: 12519, 25: 87481}, + ("Category Full House", 6, 5): {0: 6524, 25: 93476}, + ("Category Full House", 6, 6): {0: 3606, 25: 96394}, + ("Category Full House", 6, 7): {0: 1959, 25: 98041}, + ("Category Full House", 6, 8): {0: 1026, 25: 98974}, + ("Category Full House", 7, 0): {0: 100000}, + ("Category Full House", 7, 1): {0: 60232, 25: 39768}, + ("Category Full House", 7, 2): {0: 18894, 25: 81106}, + ("Category Full House", 7, 3): {0: 5682, 25: 94318}, + ("Category Full House", 7, 4): {0: 1706, 25: 98294}, + ("Category Full House", 7, 5): {0: 522, 25: 99478}, + ("Category Full House", 7, 6): {0: 146, 25: 99854}, + ("Category Full House", 7, 7): {0: 54, 25: 99946}, + ("Category Full House", 7, 8): {0: 18, 25: 99982}, + ("Category Full House", 8, 0): {0: 100000}, + ("Category Full House", 8, 1): {0: 35909, 25: 64091}, + ("Category Full House", 8, 2): {0: 5712, 25: 94288}, + ("Category Full House", 8, 3): {0: 930, 25: 99070}, + ("Category Full House", 8, 4): {0: 165, 25: 99835}, + ("Category Full House", 8, 5): {0: 19, 25: 99981}, + ("Category Full House", 8, 6): {0: 6, 25: 99994}, + ("Category Full House", 8, 7): {25: 100000}, + ("Category Full House", 8, 8): {25: 100000}, + ("Category Yacht", 0, 0): {0: 100000}, + ("Category Yacht", 0, 1): {0: 100000}, + ("Category Yacht", 0, 2): {0: 100000}, + ("Category Yacht", 0, 3): {0: 100000}, + ("Category Yacht", 0, 4): {0: 100000}, + ("Category Yacht", 0, 5): {0: 100000}, + ("Category Yacht", 0, 6): {0: 100000}, + ("Category Yacht", 0, 7): {0: 100000}, + ("Category Yacht", 0, 8): {0: 100000}, + ("Category Yacht", 1, 0): {0: 100000}, + ("Category Yacht", 1, 1): {0: 100000}, + ("Category Yacht", 1, 2): {0: 100000}, + ("Category Yacht", 1, 3): {0: 100000}, + ("Category Yacht", 1, 4): {0: 100000}, + ("Category Yacht", 1, 5): {0: 100000}, + ("Category Yacht", 1, 6): {0: 100000}, + ("Category Yacht", 1, 7): {0: 100000}, + ("Category Yacht", 1, 8): {0: 100000}, + ("Category Yacht", 2, 0): {0: 100000}, + ("Category Yacht", 2, 1): {0: 100000}, + ("Category Yacht", 2, 2): {0: 100000}, + ("Category Yacht", 2, 3): {0: 100000}, + ("Category Yacht", 2, 4): {0: 100000}, + ("Category Yacht", 2, 5): {0: 100000}, + ("Category Yacht", 2, 6): {0: 100000}, + ("Category Yacht", 2, 7): {0: 100000}, + ("Category Yacht", 2, 8): {0: 100000}, + ("Category Yacht", 3, 0): {0: 100000}, + ("Category Yacht", 3, 1): {0: 100000}, + ("Category Yacht", 3, 2): {0: 100000}, + ("Category Yacht", 3, 3): {0: 100000}, + ("Category Yacht", 3, 4): {0: 100000}, + ("Category Yacht", 3, 5): {0: 100000}, + ("Category Yacht", 3, 6): {0: 100000}, + ("Category Yacht", 3, 7): {0: 100000}, + ("Category Yacht", 3, 8): {0: 100000}, + ("Category Yacht", 4, 0): {0: 100000}, + ("Category Yacht", 4, 1): {0: 100000}, + ("Category Yacht", 4, 2): {0: 100000}, + ("Category Yacht", 4, 3): {0: 100000}, + ("Category Yacht", 4, 4): {0: 100000}, + ("Category Yacht", 4, 5): {0: 100000}, + ("Category Yacht", 4, 6): {0: 100000}, + ("Category Yacht", 4, 7): {0: 100000}, + ("Category Yacht", 4, 8): {0: 100000}, + ("Category Yacht", 5, 0): {0: 100000}, + ("Category Yacht", 5, 1): {0: 100000}, + ("Category Yacht", 5, 2): {0: 98727, 50: 1273}, + ("Category Yacht", 5, 3): {0: 95347, 50: 4653}, + ("Category Yacht", 5, 4): {0: 89969, 50: 10031}, + ("Category Yacht", 5, 5): {0: 83124, 50: 16876}, + ("Category Yacht", 5, 6): {0: 75023, 50: 24977}, + ("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: 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}, + ("Category Yacht", 6, 5): {0: 56573, 50: 43427}, + ("Category Yacht", 6, 6): {0: 44206, 50: 55794}, + ("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: 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}, + ("Category Yacht", 7, 5): {0: 33364, 50: 66636}, + ("Category Yacht", 7, 6): {0: 21483, 50: 78517}, + ("Category Yacht", 7, 7): {0: 13597, 50: 86403}, + ("Category Yacht", 7, 8): {0: 8483, 50: 91517}, + ("Category Yacht", 8, 0): {0: 100000}, + ("Category Yacht", 8, 1): {0: 97212, 50: 2788}, + ("Category Yacht", 8, 2): {0: 76962, 50: 23038}, + ("Category Yacht", 8, 3): {0: 50533, 50: 49467}, + ("Category Yacht", 8, 4): {0: 29981, 50: 70019}, + ("Category Yacht", 8, 5): {0: 16776, 50: 83224}, + ("Category Yacht", 8, 6): {0: 9079, 50: 90921}, + ("Category Yacht", 8, 7): {0: 4705, 50: 95295}, + ("Category Yacht", 8, 8): {0: 2363, 50: 97637}, + ("Category Distincts", 1, 1): {1: 100000}, + ("Category Distincts", 1, 2): {1: 100000}, + ("Category Distincts", 1, 3): {1: 100000}, + ("Category Distincts", 1, 4): {1: 100000}, + ("Category Distincts", 1, 5): {1: 100000}, + ("Category Distincts", 1, 6): {1: 100000}, + ("Category Distincts", 1, 7): {1: 100000}, + ("Category Distincts", 1, 8): {1: 100000}, + ("Category Distincts", 2, 1): {1: 16804, 2: 83196}, + ("Category Distincts", 2, 2): {1: 2686, 2: 97314}, + ("Category Distincts", 2, 3): {1: 463, 2: 99537}, + ("Category Distincts", 2, 4): {1: 66, 2: 99934}, + ("Category Distincts", 2, 5): {1: 11, 2: 99989}, + ("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: 97240}, + ("Category Distincts", 3, 2): {1: 414, 3: 84996, 2: 14590}, + ("Category Distincts", 3, 3): {1: 109, 3: 99891}, + ("Category Distincts", 3, 4): {2: 11, 3: 99989}, + ("Category Distincts", 3, 5): {3: 100000}, + ("Category Distincts", 3, 6): {3: 100000}, + ("Category Distincts", 3, 7): {3: 100000}, + ("Category Distincts", 3, 8): {3: 100000}, + ("Category Distincts", 4, 1): {1: 458, 3: 83376, 2: 16166}, + ("Category Distincts", 4, 2): {1: 26, 4: 61232, 3: 37802, 2: 940}, + ("Category Distincts", 4, 3): {2: 3, 4: 97020, 3: 2977}, + ("Category Distincts", 4, 4): {4: 100000}, + ("Category Distincts", 4, 5): {4: 100000}, + ("Category Distincts", 4, 6): {4: 100000}, + ("Category Distincts", 4, 7): {4: 100000}, + ("Category Distincts", 4, 8): {4: 100000}, + ("Category Distincts", 5, 1): {1: 159, 3: 99841}, + ("Category Distincts", 5, 2): {2: 18, 4: 88167, 3: 11815}, + ("Category Distincts", 5, 3): {4: 100000}, + ("Category Distincts", 5, 4): {5: 67650, 4: 32350}, + ("Category Distincts", 5, 5): {5: 100000}, + ("Category Distincts", 5, 6): {5: 100000}, + ("Category Distincts", 5, 7): {5: 100000}, + ("Category Distincts", 5, 8): {5: 100000}, + ("Category Distincts", 6, 1): {1: 39, 4: 74998, 3: 24963}, + ("Category Distincts", 6, 2): {2: 1, 5: 61568, 4: 37296, 3: 1135}, + ("Category Distincts", 6, 3): {5: 93157, 4: 6843}, + ("Category Distincts", 6, 4): {5: 100000}, + ("Category Distincts", 6, 5): {5: 100000}, + ("Category Distincts", 6, 6): {5: 100000}, + ("Category Distincts", 6, 7): {5: 100000}, + ("Category Distincts", 6, 8): {6: 65828, 5: 34172}, + ("Category Distincts", 7, 1): {1: 13, 4: 99987}, + ("Category Distincts", 7, 2): {5: 99580, 4: 420}, + ("Category Distincts", 7, 3): {5: 100000}, + ("Category Distincts", 7, 4): {5: 100000}, + ("Category Distincts", 7, 5): {6: 71744, 5: 28256}, + ("Category Distincts", 7, 6): {6: 100000}, + ("Category Distincts", 7, 7): {6: 100000}, + ("Category Distincts", 7, 8): {6: 100000}, + ("Category Distincts", 8, 1): {4: 100000}, + ("Category Distincts", 8, 2): {5: 99981, 4: 19}, + ("Category Distincts", 8, 3): {6: 63291, 5: 36709}, + ("Category Distincts", 8, 4): {6: 99994, 5: 6}, + ("Category Distincts", 8, 5): {6: 100000}, + ("Category Distincts", 8, 6): {6: 100000}, + ("Category Distincts", 8, 7): {6: 100000}, + ("Category Distincts", 8, 8): {6: 100000}, + ("Category Two times Ones", 0, 0): {0: 100000}, + ("Category Two times Ones", 0, 1): {0: 100000}, + ("Category Two times Ones", 0, 2): {0: 100000}, + ("Category Two times Ones", 0, 3): {0: 100000}, + ("Category Two times Ones", 0, 4): {0: 100000}, + ("Category Two times Ones", 0, 5): {0: 100000}, + ("Category Two times Ones", 0, 6): {0: 100000}, + ("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: 100000}, + ("Category Two times Ones", 1, 2): {0: 69690, 2: 30310}, + ("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}, + ("Category Two times Ones", 1, 6): {0: 33558, 2: 66442}, + ("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: 51762}, + ("Category Two times Ones", 2, 3): {0: 33290, 2: 66710}, + ("Category Two times Ones", 2, 4): {0: 23136, 2: 76864}, + ("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", 3, 0): {0: 100000}, + ("Category Two times Ones", 3, 1): {0: 58021, 2: 41979}, + ("Category Two times Ones", 3, 2): {0: 33548, 2: 66452}, + ("Category Two times Ones", 3, 3): {0: 19375, 2: 42372, 4: 38253}, + ("Category Two times Ones", 3, 4): {0: 10998, 2: 36435, 4: 52567}, + ("Category Two times Ones", 3, 5): {0: 7954, 4: 92046}, + ("Category Two times Ones", 3, 6): {0: 347, 4: 99653}, + ("Category Two times Ones", 3, 7): {0: 2, 4: 62851, 6: 37147}, + ("Category Two times Ones", 3, 8): {6: 99476, 4: 524}, + ("Category Two times Ones", 4, 0): {0: 100000}, + ("Category Two times Ones", 4, 1): {0: 48235, 2: 51765}, + ("Category Two times Ones", 4, 2): {0: 23289, 2: 40678, 4: 36033}, + ("Category Two times Ones", 4, 3): {0: 11177, 2: 32677, 4: 56146}, + ("Category Two times Ones", 4, 4): {0: 5522, 4: 60436, 6: 34042}, + ("Category Two times Ones", 4, 5): {0: 4358, 6: 95642}, + ("Category Two times Ones", 4, 6): {0: 20, 6: 99980}, + ("Category Two times Ones", 4, 7): {6: 100000}, + ("Category Two times Ones", 4, 8): {6: 65250, 8: 34750}, + ("Category Two times Ones", 5, 0): {0: 100000}, + ("Category Two times Ones", 5, 1): {0: 40028, 2: 59972}, + ("Category Two times Ones", 5, 2): {0: 16009, 2: 35901, 4: 48090}, + ("Category Two times Ones", 5, 3): {0: 6820, 4: 57489, 6: 35691}, + ("Category Two times Ones", 5, 4): {0: 5285, 6: 94715}, + ("Category Two times Ones", 5, 5): {0: 18, 6: 66613, 8: 33369}, + ("Category Two times Ones", 5, 6): {8: 99073, 6: 927}, + ("Category Two times Ones", 5, 7): {8: 100000}, + ("Category Two times Ones", 5, 8): {8: 100000}, + ("Category Two times Ones", 6, 0): {0: 100000}, + ("Category Two times Ones", 6, 1): {0: 33502, 2: 66498}, + ("Category Two times Ones", 6, 2): {0: 13681, 4: 59162, 2: 27157}, + ("Category Two times Ones", 6, 3): {0: 5486, 6: 94514}, + ("Category Two times Ones", 6, 4): {0: 190, 6: 62108, 8: 37702}, + ("Category Two times Ones", 6, 5): {8: 99882, 6: 118}, + ("Category Two times Ones", 6, 6): {8: 65144, 10: 34856}, + ("Category Two times Ones", 6, 7): {10: 99524, 8: 476}, + ("Category Two times Ones", 6, 8): {10: 100000}, + ("Category Two times Ones", 7, 0): {0: 100000}, + ("Category Two times Ones", 7, 1): {0: 27683, 2: 39060, 4: 33257}, + ("Category Two times Ones", 7, 2): {0: 8683, 4: 54932, 6: 36385}, + ("Category Two times Ones", 7, 3): {0: 373, 6: 66572, 8: 33055}, + ("Category Two times Ones", 7, 4): {8: 99816, 6: 184}, + ("Category Two times Ones", 7, 5): {8: 58124, 10: 41876}, + ("Category Two times Ones", 7, 6): {10: 99948, 8: 52}, + ("Category Two times Ones", 7, 7): {10: 62549, 12: 37451}, + ("Category Two times Ones", 7, 8): {12: 99818, 10: 182}, + ("Category Two times Ones", 8, 0): {0: 100000}, + ("Category Two times Ones", 8, 1): {0: 23378, 2: 37157, 4: 39465}, + ("Category Two times Ones", 8, 2): {0: 5602, 6: 94398}, + ("Category Two times Ones", 8, 3): {0: 8, 6: 10911, 8: 89081}, + ("Category Two times Ones", 8, 4): {8: 59809, 10: 40191}, + ("Category Two times Ones", 8, 5): {10: 68808, 12: 31114, 8: 78}, + ("Category Two times Ones", 8, 6): {12: 98712, 10: 1287, 8: 1}, + ("Category Two times Ones", 8, 7): {12: 100000}, + ("Category Two times Ones", 8, 8): {12: 59018, 14: 40982}, + ("Category Half of Sixes", 0, 0): {0: 100000}, + ("Category Half of Sixes", 0, 1): {0: 100000}, + ("Category Half of Sixes", 0, 2): {0: 100000}, + ("Category Half of Sixes", 0, 3): {0: 100000}, + ("Category Half of Sixes", 0, 4): {0: 100000}, + ("Category Half of Sixes", 0, 5): {0: 100000}, + ("Category Half of Sixes", 0, 6): {0: 100000}, + ("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: 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}, + ("Category Half of Sixes", 1, 5): {0: 40271, 3: 59729}, + ("Category Half of Sixes", 1, 6): {0: 33201, 3: 66799}, + ("Category Half of Sixes", 1, 7): {0: 27903, 3: 72097}, + ("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: 51798}, + ("Category Half of Sixes", 2, 3): {0: 33376, 3: 66624}, + ("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", 3, 0): {0: 100000}, + ("Category Half of Sixes", 3, 1): {0: 57964, 3: 42036}, + ("Category Half of Sixes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, + ("Category Half of Sixes", 3, 3): {0: 19520, 3: 42382, 6: 38098}, + ("Category Half of Sixes", 3, 4): {0: 11265, 3: 35772, 6: 52963}, + ("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: 1317, 6: 30047, 9: 68636}, + ("Category Half of Sixes", 3, 8): {0: 750, 9: 99250}, + ("Category Half of Sixes", 4, 0): {0: 100000}, + ("Category Half of Sixes", 4, 1): {0: 48121, 3: 51879}, + ("Category Half of Sixes", 4, 2): {0: 23296, 3: 40989, 6: 35715}, + ("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: 33799}, + ("Category Half of Sixes", 4, 5): {0: 5225, 6: 29678, 9: 65097}, + ("Category Half of Sixes", 4, 6): {0: 3535, 9: 96465}, + ("Category Half of Sixes", 4, 7): {0: 6, 9: 72939, 12: 27055}, + ("Category Half of Sixes", 4, 8): {9: 25326, 12: 74674}, + ("Category Half of Sixes", 5, 0): {0: 100000}, + ("Category Half of Sixes", 5, 1): {0: 40183, 3: 59817}, + ("Category Half of Sixes", 5, 2): {0: 16197, 3: 35494, 6: 48309}, + ("Category Half of Sixes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 35591}, + ("Category Half of Sixes", 5, 4): {0: 5007, 6: 25159, 9: 49038, 12: 20796}, + ("Category Half of Sixes", 5, 5): {0: 2900, 9: 38935, 12: 58165}, + ("Category Half of Sixes", 5, 6): {0: 2090, 12: 97910}, + ("Category Half of Sixes", 5, 7): {12: 99994, 9: 6}, + ("Category Half of Sixes", 5, 8): {12: 73524, 15: 26476}, + ("Category Half of Sixes", 6, 0): {0: 100000}, + ("Category Half of Sixes", 6, 1): {0: 33473, 3: 40175, 6: 26352}, + ("Category Half of Sixes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 26631}, + ("Category Half of Sixes", 6, 3): {0: 2460, 6: 21148, 9: 55356, 12: 21036}, + ("Category Half of Sixes", 6, 4): {0: 997, 9: 29741, 12: 69262}, + ("Category Half of Sixes", 6, 5): {0: 831, 12: 76328, 15: 22841}, + ("Category Half of Sixes", 6, 6): {12: 29960, 15: 70040}, + ("Category Half of Sixes", 6, 7): {15: 100000}, + ("Category Half of Sixes", 6, 8): {15: 79456, 18: 20544}, + ("Category Half of Sixes", 7, 0): {0: 100000}, + ("Category Half of Sixes", 7, 1): {0: 27933, 3: 39105, 6: 32962}, + ("Category Half of Sixes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 36478}, + ("Category Half of Sixes", 7, 3): {0: 1321, 9: 40251, 12: 58428}, + ("Category Half of Sixes", 7, 4): {0: 370, 12: 74039, 15: 25591}, + ("Category Half of Sixes", 7, 5): {0: 6, 15: 98660, 12: 1334}, + ("Category Half of Sixes", 7, 6): {15: 73973, 18: 26027}, + ("Category Half of Sixes", 7, 7): {18: 100000}, + ("Category Half of Sixes", 7, 8): {18: 100000}, + ("Category Half of Sixes", 8, 0): {0: 100000}, + ("Category Half of Sixes", 8, 1): {0: 23337, 3: 37232, 6: 39431}, + ("Category Half of Sixes", 8, 2): {0: 4652, 6: 29310, 9: 45517, 12: 20521}, + ("Category Half of Sixes", 8, 3): {0: 1300, 12: 77919, 15: 20781}, + ("Category Half of Sixes", 8, 4): {0: 21, 15: 98678, 12: 1301}, + ("Category Half of Sixes", 8, 5): {15: 68893, 18: 31107}, + ("Category Half of Sixes", 8, 6): {18: 100000}, + ("Category Half of Sixes", 8, 7): {18: 69986, 21: 30014}, + ("Category Half of Sixes", 8, 8): {21: 98839, 18: 1161}, + ("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: 46223, 2: 53777}, + ("Category Twos and Threes", 1, 4): {0: 38552, 2: 61448}, + ("Category Twos and Threes", 1, 5): {0: 32320, 2: 67680}, + ("Category Twos and Threes", 1, 6): {0: 10797, 3: 66593, 2: 22610}, + ("Category Twos and Threes", 1, 7): {0: 9307, 3: 90693}, + ("Category Twos and Threes", 1, 8): {0: 2173, 3: 97827}, + ("Category Twos and Threes", 2, 1): {0: 44565, 2: 55435}, + ("Category Twos and Threes", 2, 2): {0: 30855, 2: 69145}, + ("Category Twos and Threes", 2, 3): {0: 9977, 3: 67663, 2: 22360}, + ("Category Twos and Threes", 2, 4): {0: 7252, 3: 92748}, + ("Category Twos and Threes", 2, 5): {0: 1135, 3: 98865}, + ("Category Twos and Threes", 2, 6): {0: 121, 3: 99879}, + ("Category Twos and Threes", 2, 7): {2: 48, 5: 60169, 3: 39783}, + ("Category Twos and Threes", 2, 8): {5: 99998, 3: 2}, + ("Category Twos and Threes", 3, 1): {0: 29892, 2: 70108}, + ("Category Twos and Threes", 3, 2): {0: 8977, 3: 69968, 2: 21055}, + ("Category Twos and Threes", 3, 3): {0: 5237, 3: 94763}, + ("Category Twos and Threes", 3, 4): {2: 1781, 5: 65980, 3: 32239}, + ("Category Twos and Threes", 3, 5): {2: 609, 6: 65803, 5: 22563, 3: 11025}, + ("Category Twos and Threes", 3, 6): {6: 100000}, + ("Category Twos and Threes", 3, 7): {6: 100000}, + ("Category Twos and Threes", 3, 8): {6: 100000}, + ("Category Twos and Threes", 4, 1): {0: 11769, 3: 60627, 2: 27604}, + ("Category Twos and Threes", 4, 2): {2: 15639, 4: 60280, 3: 24081}, + ("Category Twos and Threes", 4, 3): {5: 72517, 2: 4298, 4: 16567, 3: 6618}, + ("Category Twos and Threes", 4, 4): {6: 73910, 5: 18921, 2: 1121, 4: 4322, 3: 1726}, + ("Category Twos and Threes", 4, 5): {2: 430, 7: 61608, 6: 28377, 5: 7264, 4: 1659, 3: 662}, + ("Category Twos and Threes", 4, 6): {9: 60343, 7: 24434, 6: 15223}, + ("Category Twos and Threes", 4, 7): {9: 100000}, + ("Category Twos and Threes", 4, 8): {9: 100000}, + ("Category Twos and Threes", 5, 1): {0: 11610, 3: 88390}, + ("Category Twos and Threes", 5, 2): {5: 70562, 3: 11158, 2: 534, 4: 17746}, + ("Category Twos and Threes", 5, 3): {6: 74716, 5: 23240, 3: 774, 2: 37, 4: 1233}, + ("Category Twos and Threes", 5, 4): {8: 68531, 6: 29461, 5: 1962, 3: 18, 4: 28}, + ("Category Twos and Threes", 5, 5): {9: 70635, 8: 26461, 6: 2860, 5: 44}, + ("Category Twos and Threes", 5, 6): {9: 100000}, + ("Category Twos and Threes", 5, 7): {11: 67606, 9: 32394}, + ("Category Twos and Threes", 5, 8): {12: 68354, 11: 21395, 9: 10251}, + ("Category Twos and Threes", 6, 1): {2: 4096, 4: 64713, 3: 31191}, + ("Category Twos and Threes", 6, 2): {2: 169, 6: 68210, 5: 22433, 3: 3547, 4: 5641}, + ("Category Twos and Threes", 6, 3): {2: 11, 8: 68425, 6: 23593, 5: 7338, 3: 244, 4: 389}, + ("Category Twos and Threes", 6, 4): {9: 73054, 8: 26109, 6: 787, 5: 50}, + ("Category Twos and Threes", 6, 5): {8: 8568, 11: 68223, 9: 23209}, + ("Category Twos and Threes", 6, 6): {12: 70373, 11: 20213, 9: 9414}, + ("Category Twos and Threes", 6, 7): {12: 100000}, + ("Category Twos and Threes", 6, 8): {14: 68062, 12: 31938}, + ("Category Twos and Threes", 7, 1): {2: 1390, 5: 66048, 4: 21972, 3: 10590}, + ("Category Twos and Threes", 7, 2): {2: 22, 8: 60665, 5: 11253, 6: 26834, 3: 473, 4: 753}, + ("Category Twos and Threes", 7, 3): {9: 70126, 8: 26169, 5: 909, 6: 2772, 3: 9, 4: 15}, + ("Category Twos and Threes", 7, 4): {11: 70543, 9: 28824, 8: 633}, + ("Category Twos and Threes", 7, 5): {12: 74745, 11: 22893, 9: 2173, 8: 189}, + ("Category Twos and Threes", 7, 6): {11: 7636, 14: 69766, 12: 22598}, + ("Category Twos and Threes", 7, 7): {15: 71620, 14: 19800, 12: 8580}, + ("Category Twos and Threes", 7, 8): {14: 10952, 16: 61407, 15: 27641}, + ("Category Twos and Threes", 8, 1): {2: 555, 6: 60067, 5: 26375, 4: 8774, 3: 4229}, + ("Category Twos and Threes", 8, 2): {8: 99967, 2: 13, 6: 20}, + ("Category Twos and Threes", 8, 3): {8: 10167, 11: 65964, 9: 23869}, + ("Category Twos and Threes", 8, 4): {11: 37966, 13: 62034}, + ("Category Twos and Threes", 8, 5): {11: 9059, 15: 64126, 12: 26815}, + ("Category Twos and Threes", 8, 6): {14: 14139, 17: 60581, 11: 2, 15: 25278}, + ("Category Twos and Threes", 8, 7): {14: 5173, 18: 63415, 17: 22164, 15: 9248}, + ("Category Twos and Threes", 8, 8): {18: 100000}, + ("Category Sum of Odds", 1, 1): {0: 66572, 3: 33428}, + ("Category Sum of Odds", 1, 2): {0: 44489, 3: 55511}, + ("Category Sum of Odds", 1, 3): {0: 26778, 3: 33412, 5: 39810}, + ("Category Sum of Odds", 1, 4): {0: 18191, 5: 81809}, + ("Category Sum of Odds", 1, 5): {0: 2299, 5: 97701}, + ("Category Sum of Odds", 1, 6): {0: 101, 5: 99899}, + ("Category Sum of Odds", 1, 7): {5: 100000}, + ("Category Sum of Odds", 1, 8): {5: 100000}, + ("Category Sum of Odds", 2, 1): {0: 66571, 3: 33429}, + ("Category Sum of Odds", 2, 2): {0: 38206, 4: 61794}, + ("Category Sum of Odds", 2, 3): {3: 15100, 8: 34337, 4: 24422, 5: 26141}, + ("Category Sum of Odds", 2, 4): {3: 4389, 8: 75870, 5: 19741}, + ("Category Sum of Odds", 2, 5): {8: 66180, 10: 33820}, + ("Category Sum of Odds", 2, 6): {10: 99075, 8: 925}, + ("Category Sum of Odds", 2, 7): {10: 100000}, + ("Category Sum of Odds", 2, 8): {10: 100000}, + ("Category Sum of Odds", 3, 1): {0: 19440, 3: 80560}, + ("Category Sum of Odds", 3, 2): {0: 3843, 3: 30607, 6: 65550}, + ("Category Sum of Odds", 3, 3): {8: 99451, 3: 126, 4: 204, 5: 219}, + ("Category Sum of Odds", 3, 4): {8: 39493, 9: 60507}, + ("Category Sum of Odds", 3, 5): {8: 25186, 13: 36226, 9: 38588}, + ("Category Sum of Odds", 3, 6): {13: 99387, 8: 242, 9: 371}, + ("Category Sum of Odds", 3, 7): {13: 63989, 15: 36011}, + ("Category Sum of Odds", 3, 8): {15: 99350, 13: 650}, + ("Category Sum of Odds", 4, 1): {0: 7100, 3: 29425, 5: 63475}, + ("Category Sum of Odds", 4, 2): {0: 1227, 3: 30702, 8: 68071}, + ("Category Sum of Odds", 4, 3): {8: 34941, 10: 65059}, + ("Category Sum of Odds", 4, 4): {8: 30671, 11: 69329}, + ("Category Sum of Odds", 4, 5): {8: 20766, 13: 79234}, + ("Category Sum of Odds", 4, 6): {13: 67313, 18: 32687}, + ("Category Sum of Odds", 4, 7): {13: 12063, 18: 87937}, + ("Category Sum of Odds", 4, 8): {18: 66936, 20: 33064}, + ("Category Sum of Odds", 5, 1): {0: 2404, 3: 31470, 6: 66126}, + ("Category Sum of Odds", 5, 2): {6: 12689, 11: 60256, 8: 27055}, + ("Category Sum of Odds", 5, 3): {10: 36853, 13: 63147}, + ("Category Sum of Odds", 5, 4): {13: 38005, 15: 61994, 10: 1}, + ("Category Sum of Odds", 5, 5): {13: 33747, 16: 66253}, + ("Category Sum of Odds", 5, 6): {13: 23587, 18: 76413}, + ("Category Sum of Odds", 5, 7): {18: 67776, 23: 32224}, + ("Category Sum of Odds", 5, 8): {23: 99176, 18: 824}, + ("Category Sum of Odds", 6, 1): {0: 791, 3: 32146, 7: 67063}, + ("Category Sum of Odds", 6, 2): {11: 38567, 13: 61432, 8: 1}, + ("Category Sum of Odds", 6, 3): {15: 65880, 11: 5075, 13: 29045}, + ("Category Sum of Odds", 6, 4): {15: 37367, 18: 62633}, + ("Category Sum of Odds", 6, 5): {18: 38038, 20: 61948, 15: 14}, + ("Category Sum of Odds", 6, 6): {18: 33838, 21: 66162}, + ("Category Sum of Odds", 6, 7): {18: 16130, 23: 83870}, + ("Category Sum of Odds", 6, 8): {23: 66748, 28: 33252}, + ("Category Sum of Odds", 7, 1): {5: 12019, 9: 63507, 7: 24474}, + ("Category Sum of Odds", 7, 2): {11: 37365, 15: 62635}, + ("Category Sum of Odds", 7, 3): {15: 36250, 18: 63750}, + ("Category Sum of Odds", 7, 4): {18: 37627, 21: 62373}, + ("Category Sum of Odds", 7, 5): {20: 35127, 23: 64873}, + ("Category Sum of Odds", 7, 6): {20: 12629, 25: 64047, 23: 23324}, + ("Category Sum of Odds", 7, 7): {23: 32409, 26: 67591}, + ("Category Sum of Odds", 7, 8): {23: 22322, 28: 77678}, + ("Category Sum of Odds", 8, 1): {5: 4088, 10: 65985, 9: 21602, 7: 8325}, + ("Category Sum of Odds", 8, 2): {13: 35686, 17: 64314}, + ("Category Sum of Odds", 8, 3): {17: 13770, 21: 62013, 18: 24217}, + ("Category Sum of Odds", 8, 4): {21: 37763, 24: 62237}, + ("Category Sum of Odds", 8, 5): {23: 12631, 26: 66541, 21: 4, 24: 20824}, + ("Category Sum of Odds", 8, 6): {23: 4929, 29: 60982, 26: 25964, 24: 8125}, + ("Category Sum of Odds", 8, 7): {23: 1608, 30: 67370, 29: 19899, 26: 8472, 24: 2651}, + ("Category Sum of Odds", 8, 8): {28: 4861, 32: 61811, 30: 25729, 29: 7599}, + ("Category Sum of Evens", 1, 1): {0: 66318, 4: 33682}, + ("Category Sum of Evens", 1, 2): {0: 44331, 4: 55669}, + ("Category Sum of Evens", 1, 3): {0: 29576, 4: 35040, 6: 35384}, + ("Category Sum of Evens", 1, 4): {0: 22612, 6: 77388}, + ("Category Sum of Evens", 1, 5): {0: 3566, 6: 96434}, + ("Category Sum of Evens", 1, 6): {0: 209, 6: 99791}, + ("Category Sum of Evens", 1, 7): {0: 3, 6: 99997}, + ("Category Sum of Evens", 1, 8): {6: 100000}, + ("Category Sum of Evens", 2, 1): {0: 25229, 2: 36083, 6: 38688}, + ("Category Sum of Evens", 2, 2): {0: 57, 4: 38346, 8: 37232, 2: 81, 6: 24284}, + ("Category Sum of Evens", 2, 3): {6: 39504, 10: 37060, 4: 1, 8: 23435}, + ("Category Sum of Evens", 2, 4): {10: 99495, 6: 317, 8: 188}, + ("Category Sum of Evens", 2, 5): {10: 69597, 12: 30403}, + ("Category Sum of Evens", 2, 6): {12: 98377, 10: 1623}, + ("Category Sum of Evens", 2, 7): {12: 100000}, + ("Category Sum of Evens", 2, 8): {12: 100000}, + ("Category Sum of Evens", 3, 1): {0: 76, 4: 38332, 8: 37178, 2: 109, 6: 24305}, + ("Category Sum of Evens", 3, 2): {8: 67248, 12: 32556, 4: 196}, + ("Category Sum of Evens", 3, 3): {10: 44843, 14: 33195, 8: 213, 12: 21749}, + ("Category Sum of Evens", 3, 4): {10: 37288, 14: 62712}, + ("Category Sum of Evens", 3, 5): {14: 61196, 16: 38802, 10: 2}, + ("Category Sum of Evens", 3, 6): {16: 99621, 14: 379}, + ("Category Sum of Evens", 3, 7): {16: 67674, 18: 32326}, + ("Category Sum of Evens", 3, 8): {18: 100000}, + ("Category Sum of Evens", 4, 1): {6: 37636, 10: 40039, 4: 32, 8: 22293}, + ("Category Sum of Evens", 4, 2): {10: 57689, 14: 42258, 6: 53}, + ("Category Sum of Evens", 4, 3): {14: 67801, 18: 32152, 10: 47}, + ("Category Sum of Evens", 4, 4): {18: 98878, 14: 1122}, + ("Category Sum of Evens", 4, 5): {18: 60401, 20: 39599}, + ("Category Sum of Evens", 4, 6): {20: 64396, 22: 35186, 18: 418}, + ("Category Sum of Evens", 4, 7): {22: 99697, 20: 302, 18: 1}, + ("Category Sum of Evens", 4, 8): {22: 100000}, + ("Category Sum of Evens", 5, 1): {8: 35338, 12: 41027, 6: 22, 10: 23613}, + ("Category Sum of Evens", 5, 2): {12: 37027, 18: 35856, 10: 10, 14: 27107}, + ("Category Sum of Evens", 5, 3): {18: 68230, 22: 31735, 14: 35}, + ("Category Sum of Evens", 5, 4): {18: 14880, 22: 53608, 24: 31512}, + ("Category Sum of Evens", 5, 5): {24: 98732, 18: 275, 22: 993}, + ("Category Sum of Evens", 5, 6): {24: 61498, 26: 38502}, + ("Category Sum of Evens", 5, 7): {26: 65201, 28: 34488, 24: 311}, + ("Category Sum of Evens", 5, 8): {28: 99648, 26: 351, 24: 1}, + ("Category Sum of Evens", 6, 1): {10: 34538, 14: 41426, 8: 4, 12: 24032}, + ("Category Sum of Evens", 6, 2): {16: 43552, 22: 31546, 14: 235, 12: 121, 18: 24546}, + ("Category Sum of Evens", 6, 3): {22: 68714, 26: 31239, 18: 47}, + ("Category Sum of Evens", 6, 4): {26: 59168, 28: 33835, 22: 4791, 18: 1, 24: 2205}, + ("Category Sum of Evens", 6, 5): {26: 44386, 30: 32920, 28: 22694}, + ("Category Sum of Evens", 6, 6): {30: 98992, 26: 667, 28: 341}, + ("Category Sum of Evens", 6, 7): {30: 60806, 32: 39194}, + ("Category Sum of Evens", 6, 8): {32: 64584, 34: 35252, 30: 164}, + ("Category Sum of Evens", 7, 1): {12: 40703, 18: 30507, 10: 1, 14: 28789}, + ("Category Sum of Evens", 7, 2): {22: 60249, 24: 38366, 12: 1, 18: 767, 16: 614, 14: 3}, + ("Category Sum of Evens", 7, 3): {24: 47964, 30: 30240, 22: 4, 26: 21792}, + ("Category Sum of Evens", 7, 4): {30: 63108, 32: 35114, 24: 1778}, + ("Category Sum of Evens", 7, 5): {32: 62062, 34: 37406, 30: 523, 26: 6, 28: 3}, + ("Category Sum of Evens", 7, 6): {32: 40371, 36: 35507, 34: 24122}, + ("Category Sum of Evens", 7, 7): {34: 44013, 38: 31749, 32: 4, 36: 24234}, + ("Category Sum of Evens", 7, 8): {38: 99116, 34: 570, 36: 314}, + ("Category Sum of Evens", 8, 1): {18: 66673, 20: 31528, 12: 1054, 14: 745}, + ("Category Sum of Evens", 8, 2): {22: 40918, 28: 33610, 24: 25472}, + ("Category Sum of Evens", 8, 3): {28: 40893, 32: 41346, 24: 17, 30: 17737, 26: 7}, + ("Category Sum of Evens", 8, 4): {32: 63665, 36: 36316, 28: 19}, + ("Category Sum of Evens", 8, 5): {36: 58736, 38: 40234, 32: 1030}, + ("Category Sum of Evens", 8, 6): {36: 57946, 40: 42054}, + ("Category Sum of Evens", 8, 7): {38: 34984, 42: 39622, 36: 2, 40: 25392}, + ("Category Sum of Evens", 8, 8): {42: 65137, 44: 34611, 38: 146, 40: 106}, + ("Category Double Threes and Fours", 1, 1): {0: 66749, 6: 33251}, + ("Category Double Threes and Fours", 1, 2): {0: 44675, 6: 55325}, + ("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: 33427}, + ("Category Double Threes and Fours", 2, 2): {0: 5, 6: 46088, 12: 30763, 8: 23144}, + ("Category Double Threes and Fours", 2, 3): {0: 5, 6: 30159, 12: 32725, 14: 37111}, + ("Category Double Threes and Fours", 2, 4): {6: 20533, 14: 79467}, + ("Category Double Threes and Fours", 2, 5): {14: 69789, 16: 30211}, + ("Category Double Threes and Fours", 2, 6): {16: 99978, 14: 22}, + ("Category Double Threes and Fours", 2, 7): {16: 100000}, + ("Category Double Threes and Fours", 2, 8): {16: 100000}, + ("Category Double Threes and Fours", 3, 1): {0: 8, 6: 49139, 12: 26176, 8: 24677}, + ("Category Double Threes and Fours", 3, 2): {0: 5, 6: 24942, 12: 27065, 14: 47988}, + ("Category Double Threes and Fours", 3, 3): {6: 12743, 14: 56776, 20: 30481}, + ("Category Double Threes and Fours", 3, 4): {14: 9753, 20: 90247}, + ("Category Double Threes and Fours", 3, 5): {20: 61293, 22: 38707}, + ("Category Double Threes and Fours", 3, 6): {22: 99615, 20: 385}, + ("Category Double Threes and Fours", 3, 7): {22: 67267, 24: 32733}, + ("Category Double Threes and Fours", 3, 8): {24: 100000}, + ("Category Double Threes and Fours", 4, 1): {6: 26819, 12: 39789, 14: 33392}, + ("Category Double Threes and Fours", 4, 2): {14: 63726, 20: 36011, 6: 106, 12: 157}, + ("Category Double Threes and Fours", 4, 3): {20: 69628, 24: 30158, 14: 214}, + ("Category Double Threes and Fours", 4, 4): {20: 11409, 24: 57067, 26: 31524}, + ("Category Double Threes and Fours", 4, 5): {20: 6566, 26: 57047, 28: 36387}, + ("Category Double Threes and Fours", 4, 6): {28: 63694, 30: 35203, 20: 113, 26: 990}, + ("Category Double Threes and Fours", 4, 7): {30: 98893, 28: 1092, 26: 15}, + ("Category Double Threes and Fours", 4, 8): {30: 100000}, + ("Category Double Threes and Fours", 5, 1): {6: 16042, 14: 83958}, + ("Category Double Threes and Fours", 5, 2): {14: 44329, 20: 24912, 24: 30759}, + ("Category Double Threes and Fours", 5, 3): {24: 57603, 28: 42155, 20: 242}, + ("Category Double Threes and Fours", 5, 4): {26: 32446, 30: 43875, 24: 21, 28: 23658}, + ("Category Double Threes and Fours", 5, 5): {30: 69209, 34: 30672, 26: 69, 28: 50}, + ("Category Double Threes and Fours", 5, 6): {34: 63882, 36: 35323, 30: 795}, + ("Category Double Threes and Fours", 5, 7): {36: 65178, 38: 34598, 34: 222, 30: 2}, + ("Category Double Threes and Fours", 5, 8): {38: 99654, 36: 345, 34: 1}, + ("Category Double Threes and Fours", 6, 1): {14: 68079, 18: 31921}, + ("Category Double Threes and Fours", 6, 2): {14: 14542, 24: 48679, 28: 36779}, + ("Category Double Threes and Fours", 6, 3): {28: 62757, 34: 36962, 24: 281}, + ("Category Double Threes and Fours", 6, 4): {34: 68150, 38: 30771, 28: 604, 26: 1, 30: 474}, + ("Category Double Threes and Fours", 6, 5): {38: 68332, 40: 30833, 34: 823, 28: 12}, + ("Category Double Threes and Fours", 6, 6): {40: 67631, 42: 31174, 38: 1181, 34: 14}, + ("Category Double Threes and Fours", 6, 7): {42: 63245, 44: 35699, 40: 1038, 38: 18}, + ("Category Double Threes and Fours", 6, 8): {44: 64056, 46: 35162, 42: 770, 40: 12}, + ("Category Double Threes and Fours", 7, 1): {14: 14976, 18: 54685, 22: 30339}, + ("Category Double Threes and Fours", 7, 2): {14: 10532, 28: 55372, 32: 34096}, + ("Category Double Threes and Fours", 7, 3): {32: 42786, 40: 32123, 28: 2, 34: 25089}, + ("Category Double Threes and Fours", 7, 4): {38: 46172, 44: 31648, 32: 226, 40: 21954}, + ("Category Double Threes and Fours", 7, 5): {44: 64883, 46: 34437, 38: 460, 32: 2, 40: 218}, + ("Category Double Threes and Fours", 7, 6): {44: 43458, 48: 33715, 46: 22827}, + ("Category Double Threes and Fours", 7, 7): {46: 44472, 50: 32885, 44: 15, 48: 22628}, + ("Category Double Threes and Fours", 7, 8): {48: 41682, 52: 37868, 46: 18, 50: 20432}, + ("Category Double Threes and Fours", 8, 1): {14: 14227, 22: 85773}, + ("Category Double Threes and Fours", 8, 2): {22: 7990, 32: 56319, 36: 35691}, + ("Category Double Threes and Fours", 8, 3): {32: 19914, 40: 43585, 44: 36501}, + ("Category Double Threes and Fours", 8, 4): {44: 63232, 48: 36613, 32: 48, 40: 107}, + ("Category Double Threes and Fours", 8, 5): {48: 62939, 52: 36798, 44: 263}, + ("Category Double Threes and Fours", 8, 6): {52: 60756, 54: 38851, 48: 392, 44: 1}, + ("Category Double Threes and Fours", 8, 7): {54: 62281, 56: 37262, 52: 455, 48: 2}, + ("Category Double Threes and Fours", 8, 8): {56: 67295, 60: 32064, 54: 637, 52: 4}, + ("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: 14381, 8: 85619}, + ("Category Quadruple Ones and Twos", 1, 7): {0: 4137, 8: 95863}, + ("Category Quadruple Ones and Twos", 1, 8): {0: 1004, 8: 98996}, + ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 4: 22273, 8: 33161}, + ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 4: 24890, 8: 32262, 12: 22885}, + ("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: 6655, 8: 30200, 12: 26499, 16: 36646}, + ("Category Quadruple Ones and Twos", 2, 5): {0: 982, 8: 16426, 12: 24307, 16: 58285}, + ("Category Quadruple Ones and Twos", 2, 6): {0: 68, 8: 9887, 16: 90045}, + ("Category Quadruple Ones and Twos", 2, 7): {0: 11, 16: 99989}, + ("Category Quadruple Ones and Twos", 2, 8): {16: 100000}, + ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 4: 22574, 8: 27747, 12: 20239}, + ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 4: 16295, 8: 26434, 12: 22986, 16: 25428}, + ("Category Quadruple Ones and Twos", 3, 3): {0: 3649, 8: 15314, 12: 24619, 16: 38944, 20: 17474}, + ("Category Quadruple Ones and Twos", 3, 4): {0: 11, 8: 8430, 16: 41259, 20: 50300}, + ("Category Quadruple Ones and Twos", 3, 5): {20: 80030, 24: 19902, 8: 11, 16: 57}, + ("Category Quadruple Ones and Twos", 3, 6): {20: 23895, 24: 76105}, + ("Category Quadruple Ones and Twos", 3, 7): {24: 100000}, + ("Category Quadruple Ones and Twos", 3, 8): {24: 100000}, + ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 4: 19657, 8: 27288, 12: 16126, 16: 17238}, + ("Category Quadruple Ones and Twos", 4, 2): {0: 1222, 4: 15703, 12: 24015, 16: 34944, 20: 24116}, + ("Category Quadruple Ones and Twos", 4, 3): {0: 227, 12: 14519, 20: 62257, 24: 22997}, + ("Category Quadruple Ones and Twos", 4, 4): {0: 11, 20: 17266, 24: 67114, 28: 15609}, + ("Category Quadruple Ones and Twos", 4, 5): {24: 27365, 28: 72632, 20: 3}, + ("Category Quadruple Ones and Twos", 4, 6): {28: 81782, 32: 18215, 24: 3}, + ("Category Quadruple Ones and Twos", 4, 7): {28: 22319, 32: 77681}, + ("Category Quadruple Ones and Twos", 4, 8): {32: 100000}, + ("Category Quadruple Ones and Twos", 5, 1): {0: 13112, 4: 16534, 8: 24718, 12: 18558, 16: 27078}, + ("Category Quadruple Ones and Twos", 5, 2): {0: 21, 4: 15200, 16: 28784, 20: 32131, 24: 23864}, + ("Category Quadruple Ones and Twos", 5, 3): {0: 4, 16: 8475, 24: 66718, 28: 24803}, + ("Category Quadruple Ones and Twos", 5, 4): {28: 76149, 32: 23289, 24: 550, 20: 12}, + ("Category Quadruple Ones and Twos", 5, 5): {32: 81110, 36: 16222, 28: 2663, 24: 5}, + ("Category Quadruple Ones and Twos", 5, 6): {32: 18542, 36: 81458}, + ("Category Quadruple Ones and Twos", 5, 7): {36: 82036, 40: 17964}, + ("Category Quadruple Ones and Twos", 5, 8): {36: 27864, 40: 72136}, + ("Category Quadruple Ones and Twos", 6, 1): {0: 6419, 8: 16963, 12: 22116, 16: 33903, 20: 20599}, + ("Category Quadruple Ones and Twos", 6, 2): {0: 5, 16: 8913, 24: 67749, 28: 23333}, + ("Category Quadruple Ones and Twos", 6, 3): {28: 71779, 32: 27514, 16: 82, 24: 625}, + ("Category Quadruple Ones and Twos", 6, 4): {32: 72333, 36: 27328, 28: 337, 24: 2}, + ("Category Quadruple Ones and Twos", 6, 5): {36: 73993, 40: 25138, 32: 865, 28: 4}, + ("Category Quadruple Ones and Twos", 6, 6): {40: 80918, 44: 17126, 36: 1934, 32: 22}, + ("Category Quadruple Ones and Twos", 6, 7): {40: 20298, 44: 79702}, + ("Category Quadruple Ones and Twos", 6, 8): {44: 81077, 48: 18923}, + ("Category Quadruple Ones and Twos", 7, 1): {0: 508, 8: 10298, 16: 41828, 20: 30853, 24: 16513}, + ("Category Quadruple Ones and Twos", 7, 2): {16: 7429, 28: 69817, 32: 22754}, + ("Category Quadruple Ones and Twos", 7, 3): {32: 82871, 40: 16531, 16: 57, 28: 541}, + ("Category Quadruple Ones and Twos", 7, 4): {36: 67601, 44: 17916, 32: 909, 40: 13569, 28: 5}, + ("Category Quadruple Ones and Twos", 7, 5): {40: 67395, 48: 17447, 36: 364, 44: 14790, 32: 4}, + ("Category Quadruple Ones and Twos", 7, 6): {48: 91242, 40: 7151, 36: 38, 44: 1569}, + ("Category Quadruple Ones and Twos", 7, 7): {48: 80854, 52: 19146}, + ("Category Quadruple Ones and Twos", 7, 8): {48: 25334, 52: 74666}, + ("Category Quadruple Ones and Twos", 8, 1): {0: 119, 16: 17496, 20: 26705, 24: 55680}, + ("Category Quadruple Ones and Twos", 8, 2): {24: 569, 32: 72257, 36: 21817, 28: 5357}, + ("Category Quadruple Ones and Twos", 8, 3): {36: 66654, 44: 18473, 32: 1396, 40: 13477}, + ("Category Quadruple Ones and Twos", 8, 4): {44: 73954, 48: 22240, 36: 3178, 40: 628}, + ("Category Quadruple Ones and Twos", 8, 5): {48: 76082, 52: 22415, 44: 1500, 36: 3}, + ("Category Quadruple Ones and Twos", 8, 6): {52: 74901, 56: 21332, 48: 3766, 44: 1}, + ("Category Quadruple Ones and Twos", 8, 7): {56: 96171, 52: 3640, 48: 189}, + ("Category Quadruple Ones and Twos", 8, 8): {56: 78035, 60: 21965}, + ("Category Micro Straight", 1, 1): {0: 100000}, + ("Category Micro Straight", 1, 2): {0: 100000}, + ("Category Micro Straight", 1, 3): {0: 100000}, + ("Category Micro Straight", 1, 4): {0: 100000}, + ("Category Micro Straight", 1, 5): {0: 100000}, + ("Category Micro Straight", 1, 6): {0: 100000}, + ("Category Micro Straight", 1, 7): {0: 100000}, + ("Category Micro Straight", 1, 8): {0: 100000}, + ("Category Micro Straight", 2, 1): {0: 72326, 10: 27674}, + ("Category Micro Straight", 2, 2): {0: 48546, 10: 51454}, + ("Category Micro Straight", 2, 3): {0: 32619, 10: 67381}, + ("Category Micro Straight", 2, 4): {0: 21659, 10: 78341}, + ("Category Micro Straight", 2, 5): {0: 14288, 10: 85712}, + ("Category Micro Straight", 2, 6): {0: 9882, 10: 90118}, + ("Category Micro Straight", 2, 7): {0: 6502, 10: 93498}, + ("Category Micro Straight", 2, 8): {0: 4161, 10: 95839}, + ("Category Micro Straight", 3, 1): {0: 41943, 10: 58057}, + ("Category Micro Straight", 3, 2): {0: 15524, 10: 84476}, + ("Category Micro Straight", 3, 3): {0: 5700, 10: 94300}, + ("Category Micro Straight", 3, 4): {0: 2127, 10: 97873}, + ("Category Micro Straight", 3, 5): {0: 744, 10: 99256}, + ("Category Micro Straight", 3, 6): {0: 260, 10: 99740}, + ("Category Micro Straight", 3, 7): {0: 115, 10: 99885}, + ("Category Micro Straight", 3, 8): {0: 34, 10: 99966}, + ("Category Micro Straight", 4, 1): {0: 22307, 10: 77693}, + ("Category Micro Straight", 4, 2): {0: 4420, 10: 95580}, + ("Category Micro Straight", 4, 3): {0: 806, 10: 99194}, + ("Category Micro Straight", 4, 4): {0: 205, 10: 99795}, + ("Category Micro Straight", 4, 5): {0: 20, 10: 99980}, + ("Category Micro Straight", 4, 6): {0: 5, 10: 99995}, + ("Category Micro Straight", 4, 7): {0: 1, 10: 99999}, + ("Category Micro Straight", 4, 8): {0: 1, 10: 99999}, + ("Category Micro Straight", 5, 1): {0: 11685, 10: 88315}, + ("Category Micro Straight", 5, 2): {0: 1141, 10: 98859}, + ("Category Micro Straight", 5, 3): {0: 119, 10: 99881}, + ("Category Micro Straight", 5, 4): {0: 11, 10: 99989}, + ("Category Micro Straight", 5, 5): {0: 1, 10: 99999}, + ("Category Micro Straight", 5, 6): {10: 100000}, + ("Category Micro Straight", 5, 7): {10: 100000}, + ("Category Micro Straight", 5, 8): {10: 100000}, + ("Category Micro Straight", 6, 1): {0: 5937, 10: 94063}, + ("Category Micro Straight", 6, 2): {0: 307, 10: 99693}, + ("Category Micro Straight", 6, 3): {0: 9, 10: 99991}, + ("Category Micro Straight", 6, 4): {0: 1, 10: 99999}, + ("Category Micro Straight", 6, 5): {10: 100000}, + ("Category Micro Straight", 6, 6): {10: 100000}, + ("Category Micro Straight", 6, 7): {10: 100000}, + ("Category Micro Straight", 6, 8): {10: 100000}, + ("Category Micro Straight", 7, 1): {0: 3072, 10: 96928}, + ("Category Micro Straight", 7, 2): {0: 85, 10: 99915}, + ("Category Micro Straight", 7, 3): {0: 2, 10: 99998}, + ("Category Micro Straight", 7, 4): {10: 100000}, + ("Category Micro Straight", 7, 5): {10: 100000}, + ("Category Micro Straight", 7, 6): {10: 100000}, + ("Category Micro Straight", 7, 7): {10: 100000}, + ("Category Micro Straight", 7, 8): {10: 100000}, + ("Category Micro Straight", 8, 1): {0: 1544, 10: 98456}, + ("Category Micro Straight", 8, 2): {0: 15, 10: 99985}, + ("Category Micro Straight", 8, 3): {10: 100000}, + ("Category Micro Straight", 8, 4): {10: 100000}, + ("Category Micro Straight", 8, 5): {10: 100000}, + ("Category Micro Straight", 8, 6): {10: 100000}, + ("Category Micro Straight", 8, 7): {10: 100000}, + ("Category Micro Straight", 8, 8): {10: 100000}, + ("Category Three Odds", 1, 1): {0: 100000}, + ("Category Three Odds", 1, 2): {0: 100000}, + ("Category Three Odds", 1, 3): {0: 100000}, + ("Category Three Odds", 1, 4): {0: 100000}, + ("Category Three Odds", 1, 5): {0: 100000}, + ("Category Three Odds", 1, 6): {0: 100000}, + ("Category Three Odds", 1, 7): {0: 100000}, + ("Category Three Odds", 1, 8): {0: 100000}, + ("Category Three Odds", 2, 1): {0: 100000}, + ("Category Three Odds", 2, 2): {0: 100000}, + ("Category Three Odds", 2, 3): {0: 100000}, + ("Category Three Odds", 2, 4): {0: 100000}, + ("Category Three Odds", 2, 5): {0: 100000}, + ("Category Three Odds", 2, 6): {0: 100000}, + ("Category Three Odds", 2, 7): {0: 100000}, + ("Category Three Odds", 2, 8): {0: 100000}, + ("Category Three Odds", 3, 1): {0: 87592, 20: 12408}, + ("Category Three Odds", 3, 2): {0: 57855, 20: 42145}, + ("Category Three Odds", 3, 3): {0: 32668, 20: 67332}, + ("Category Three Odds", 3, 4): {0: 17508, 20: 82492}, + ("Category Three Odds", 3, 5): {0: 9156, 20: 90844}, + ("Category Three Odds", 3, 6): {0: 4572, 20: 95428}, + ("Category Three Odds", 3, 7): {0: 2325, 20: 97675}, + ("Category Three Odds", 3, 8): {0: 1116, 20: 98884}, + ("Category Three Odds", 4, 1): {0: 68669, 20: 31331}, + ("Category Three Odds", 4, 2): {0: 26140, 20: 73860}, + ("Category Three Odds", 4, 3): {0: 7837, 20: 92163}, + ("Category Three Odds", 4, 4): {0: 2169, 20: 97831}, + ("Category Three Odds", 4, 5): {0: 516, 20: 99484}, + ("Category Three Odds", 4, 6): {0: 156, 20: 99844}, + ("Category Three Odds", 4, 7): {0: 40, 20: 99960}, + ("Category Three Odds", 4, 8): {0: 12, 20: 99988}, + ("Category Three Odds", 5, 1): {0: 49908, 20: 50092}, + ("Category Three Odds", 5, 2): {0: 10373, 20: 89627}, + ("Category Three Odds", 5, 3): {0: 1640, 20: 98360}, + ("Category Three Odds", 5, 4): {0: 223, 20: 99777}, + ("Category Three Odds", 5, 5): {0: 24, 20: 99976}, + ("Category Three Odds", 5, 6): {0: 3, 20: 99997}, + ("Category Three Odds", 5, 7): {0: 1, 20: 99999}, + ("Category Three Odds", 5, 8): {20: 100000}, + ("Category Three Odds", 6, 1): {0: 34566, 20: 65434}, + ("Category Three Odds", 6, 2): {0: 3766, 20: 96234}, + ("Category Three Odds", 6, 3): {0: 291, 20: 99709}, + ("Category Three Odds", 6, 4): {0: 22, 20: 99978}, + ("Category Three Odds", 6, 5): {20: 100000}, + ("Category Three Odds", 6, 6): {20: 100000}, + ("Category Three Odds", 6, 7): {20: 100000}, + ("Category Three Odds", 6, 8): {20: 100000}, + ("Category Three Odds", 7, 1): {0: 22722, 20: 77278}, + ("Category Three Odds", 7, 2): {0: 1291, 20: 98709}, + ("Category Three Odds", 7, 3): {0: 38, 20: 99962}, + ("Category Three Odds", 7, 4): {0: 2, 20: 99998}, + ("Category Three Odds", 7, 5): {20: 100000}, + ("Category Three Odds", 7, 6): {20: 100000}, + ("Category Three Odds", 7, 7): {20: 100000}, + ("Category Three Odds", 7, 8): {20: 100000}, + ("Category Three Odds", 8, 1): {0: 14556, 20: 85444}, + ("Category Three Odds", 8, 2): {0: 430, 20: 99570}, + ("Category Three Odds", 8, 3): {0: 3, 20: 99997}, + ("Category Three Odds", 8, 4): {20: 100000}, + ("Category Three Odds", 8, 5): {20: 100000}, + ("Category Three Odds", 8, 6): {20: 100000}, + ("Category Three Odds", 8, 7): {20: 100000}, + ("Category Three Odds", 8, 8): {20: 100000}, + ("Category 1-2-1 Consecutive", 1, 1): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 2): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 3): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 4): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 5): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 6): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 7): {0: 100000}, + ("Category 1-2-1 Consecutive", 1, 8): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 1): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 2): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 3): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 4): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 5): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 6): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 7): {0: 100000}, + ("Category 1-2-1 Consecutive", 2, 8): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 1): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 2): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 3): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 4): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 5): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 6): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 7): {0: 100000}, + ("Category 1-2-1 Consecutive", 3, 8): {0: 100000}, + ("Category 1-2-1 Consecutive", 4, 1): {0: 96371, 30: 3629}, + ("Category 1-2-1 Consecutive", 4, 2): {0: 86605, 30: 13395}, + ("Category 1-2-1 Consecutive", 4, 3): {0: 75037, 30: 24963}, + ("Category 1-2-1 Consecutive", 4, 4): {0: 63656, 30: 36344}, + ("Category 1-2-1 Consecutive", 4, 5): {0: 53869, 30: 46131}, + ("Category 1-2-1 Consecutive", 4, 6): {0: 45131, 30: 54869}, + ("Category 1-2-1 Consecutive", 4, 7): {0: 37535, 30: 62465}, + ("Category 1-2-1 Consecutive", 4, 8): {0: 31425, 30: 68575}, + ("Category 1-2-1 Consecutive", 5, 1): {0: 86632, 30: 13368}, + ("Category 1-2-1 Consecutive", 5, 2): {0: 62779, 30: 37221}, + ("Category 1-2-1 Consecutive", 5, 3): {0: 46034, 30: 53966}, + ("Category 1-2-1 Consecutive", 5, 4): {0: 34983, 30: 65017}, + ("Category 1-2-1 Consecutive", 5, 5): {0: 28056, 30: 71944}, + ("Category 1-2-1 Consecutive", 5, 6): {0: 23150, 30: 76850}, + ("Category 1-2-1 Consecutive", 5, 7): {0: 19577, 30: 80423}, + ("Category 1-2-1 Consecutive", 5, 8): {0: 17613, 30: 82387}, + ("Category 1-2-1 Consecutive", 6, 1): {0: 71928, 30: 28072}, + ("Category 1-2-1 Consecutive", 6, 2): {0: 40724, 30: 59276}, + ("Category 1-2-1 Consecutive", 6, 3): {0: 26723, 30: 73277}, + ("Category 1-2-1 Consecutive", 6, 4): {0: 19685, 30: 80315}, + ("Category 1-2-1 Consecutive", 6, 5): {0: 15460, 30: 84540}, + ("Category 1-2-1 Consecutive", 6, 6): {0: 12526, 30: 87474}, + ("Category 1-2-1 Consecutive", 6, 7): {0: 10014, 30: 89986}, + ("Category 1-2-1 Consecutive", 6, 8): {0: 8251, 30: 91749}, + ("Category 1-2-1 Consecutive", 7, 1): {0: 55544, 30: 44456}, + ("Category 1-2-1 Consecutive", 7, 2): {0: 24840, 30: 75160}, + ("Category 1-2-1 Consecutive", 7, 3): {0: 15102, 30: 84898}, + ("Category 1-2-1 Consecutive", 7, 4): {0: 10541, 30: 89459}, + ("Category 1-2-1 Consecutive", 7, 5): {0: 7720, 30: 92280}, + ("Category 1-2-1 Consecutive", 7, 6): {0: 5554, 30: 94446}, + ("Category 1-2-1 Consecutive", 7, 7): {0: 4106, 30: 95894}, + ("Category 1-2-1 Consecutive", 7, 8): {0: 3025, 30: 96975}, + ("Category 1-2-1 Consecutive", 8, 1): {0: 40693, 30: 59307}, + ("Category 1-2-1 Consecutive", 8, 2): {0: 14827, 30: 85173}, + ("Category 1-2-1 Consecutive", 8, 3): {0: 8195, 30: 91805}, + ("Category 1-2-1 Consecutive", 8, 4): {0: 5383, 30: 94617}, + ("Category 1-2-1 Consecutive", 8, 5): {0: 3395, 30: 96605}, + ("Category 1-2-1 Consecutive", 8, 6): {0: 2299, 30: 97701}, + ("Category 1-2-1 Consecutive", 8, 7): {0: 1412, 30: 98588}, + ("Category 1-2-1 Consecutive", 8, 8): {0: 872, 30: 99128}, + ("Category Three Distinct Dice", 1, 1): {0: 100000}, + ("Category Three Distinct Dice", 1, 2): {0: 100000}, + ("Category Three Distinct Dice", 1, 3): {0: 100000}, + ("Category Three Distinct Dice", 1, 4): {0: 100000}, + ("Category Three Distinct Dice", 1, 5): {0: 100000}, + ("Category Three Distinct Dice", 1, 6): {0: 100000}, + ("Category Three Distinct Dice", 1, 7): {0: 100000}, + ("Category Three Distinct Dice", 1, 8): {0: 100000}, + ("Category Three Distinct Dice", 2, 1): {0: 100000}, + ("Category Three Distinct Dice", 2, 2): {0: 100000}, + ("Category Three Distinct Dice", 2, 3): {0: 100000}, + ("Category Three Distinct Dice", 2, 4): {0: 100000}, + ("Category Three Distinct Dice", 2, 5): {0: 100000}, + ("Category Three Distinct Dice", 2, 6): {0: 100000}, + ("Category Three Distinct Dice", 2, 7): {0: 100000}, + ("Category Three Distinct Dice", 2, 8): {0: 100000}, + ("Category Three Distinct Dice", 3, 1): {0: 44707, 20: 55293}, + ("Category Three Distinct Dice", 3, 2): {0: 15078, 20: 84922}, + ("Category Three Distinct Dice", 3, 3): {0: 5056, 20: 94944}, + ("Category Three Distinct Dice", 3, 4): {0: 1688, 20: 98312}, + ("Category Three Distinct Dice", 3, 5): {0: 516, 20: 99484}, + ("Category Three Distinct Dice", 3, 6): {0: 182, 20: 99818}, + ("Category Three Distinct Dice", 3, 7): {0: 56, 20: 99944}, + ("Category Three Distinct Dice", 3, 8): {0: 15, 20: 99985}, + ("Category Three Distinct Dice", 4, 1): {0: 16721, 20: 83279}, + ("Category Three Distinct Dice", 4, 2): {0: 1826, 20: 98174}, + ("Category Three Distinct Dice", 4, 3): {0: 203, 20: 99797}, + ("Category Three Distinct Dice", 4, 4): {0: 18, 20: 99982}, + ("Category Three Distinct Dice", 4, 5): {0: 3, 20: 99997}, + ("Category Three Distinct Dice", 4, 6): {20: 100000}, + ("Category Three Distinct Dice", 4, 7): {20: 100000}, + ("Category Three Distinct Dice", 4, 8): {20: 100000}, + ("Category Three Distinct Dice", 5, 1): {0: 5904, 20: 94096}, + ("Category Three Distinct Dice", 5, 2): {0: 236, 20: 99764}, + ("Category Three Distinct Dice", 5, 3): {0: 12, 20: 99988}, + ("Category Three Distinct Dice", 5, 4): {20: 100000}, + ("Category Three Distinct Dice", 5, 5): {20: 100000}, + ("Category Three Distinct Dice", 5, 6): {20: 100000}, + ("Category Three Distinct Dice", 5, 7): {20: 100000}, + ("Category Three Distinct Dice", 5, 8): {20: 100000}, + ("Category Three Distinct Dice", 6, 1): {0: 1992, 20: 98008}, + ("Category Three Distinct Dice", 6, 2): {0: 21, 20: 99979}, + ("Category Three Distinct Dice", 6, 3): {20: 100000}, + ("Category Three Distinct Dice", 6, 4): {20: 100000}, + ("Category Three Distinct Dice", 6, 5): {20: 100000}, + ("Category Three Distinct Dice", 6, 6): {20: 100000}, + ("Category Three Distinct Dice", 6, 7): {20: 100000}, + ("Category Three Distinct Dice", 6, 8): {20: 100000}, + ("Category Three Distinct Dice", 7, 1): {0: 692, 20: 99308}, + ("Category Three Distinct Dice", 7, 2): {0: 4, 20: 99996}, + ("Category Three Distinct Dice", 7, 3): {20: 100000}, + ("Category Three Distinct Dice", 7, 4): {20: 100000}, + ("Category Three Distinct Dice", 7, 5): {20: 100000}, + ("Category Three Distinct Dice", 7, 6): {20: 100000}, + ("Category Three Distinct Dice", 7, 7): {20: 100000}, + ("Category Three Distinct Dice", 7, 8): {20: 100000}, + ("Category Three Distinct Dice", 8, 1): {0: 243, 20: 99757}, + ("Category Three Distinct Dice", 8, 2): {0: 1, 20: 99999}, + ("Category Three Distinct Dice", 8, 3): {20: 100000}, + ("Category Three Distinct Dice", 8, 4): {20: 100000}, + ("Category Three Distinct Dice", 8, 5): {20: 100000}, + ("Category Three Distinct Dice", 8, 6): {20: 100000}, + ("Category Three Distinct Dice", 8, 7): {20: 100000}, + ("Category Three Distinct Dice", 8, 8): {20: 100000}, + ("Category Two Pair", 1, 1): {0: 100000}, + ("Category Two Pair", 1, 2): {0: 100000}, + ("Category Two Pair", 1, 3): {0: 100000}, + ("Category Two Pair", 1, 4): {0: 100000}, + ("Category Two Pair", 1, 5): {0: 100000}, + ("Category Two Pair", 1, 6): {0: 100000}, + ("Category Two Pair", 1, 7): {0: 100000}, + ("Category Two Pair", 1, 8): {0: 100000}, + ("Category Two Pair", 2, 1): {0: 100000}, + ("Category Two Pair", 2, 2): {0: 100000}, + ("Category Two Pair", 2, 3): {0: 100000}, + ("Category Two Pair", 2, 4): {0: 100000}, + ("Category Two Pair", 2, 5): {0: 100000}, + ("Category Two Pair", 2, 6): {0: 100000}, + ("Category Two Pair", 2, 7): {0: 100000}, + ("Category Two Pair", 2, 8): {0: 100000}, + ("Category Two Pair", 3, 1): {0: 100000}, + ("Category Two Pair", 3, 2): {0: 100000}, + ("Category Two Pair", 3, 3): {0: 100000}, + ("Category Two Pair", 3, 4): {0: 100000}, + ("Category Two Pair", 3, 5): {0: 100000}, + ("Category Two Pair", 3, 6): {0: 100000}, + ("Category Two Pair", 3, 7): {0: 100000}, + ("Category Two Pair", 3, 8): {0: 100000}, + ("Category Two Pair", 4, 1): {0: 93065, 30: 6935}, + ("Category Two Pair", 4, 2): {0: 82102, 30: 17898}, + ("Category Two Pair", 4, 3): {0: 71209, 30: 28791}, + ("Category Two Pair", 4, 4): {0: 61609, 30: 38391}, + ("Category Two Pair", 4, 5): {0: 53036, 30: 46964}, + ("Category Two Pair", 4, 6): {0: 45705, 30: 54295}, + ("Category Two Pair", 4, 7): {0: 39398, 30: 60602}, + ("Category Two Pair", 4, 8): {0: 33673, 30: 66327}, + ("Category Two Pair", 5, 1): {0: 72847, 30: 27153}, + ("Category Two Pair", 5, 2): {0: 46759, 30: 53241}, + ("Category Two Pair", 5, 3): {0: 29462, 30: 70538}, + ("Category Two Pair", 5, 4): {0: 18351, 30: 81649}, + ("Category Two Pair", 5, 5): {0: 11793, 30: 88207}, + ("Category Two Pair", 5, 6): {0: 7385, 30: 92615}, + ("Category Two Pair", 5, 7): {0: 4610, 30: 95390}, + ("Category Two Pair", 5, 8): {0: 2938, 30: 97062}, + ("Category Two Pair", 6, 1): {0: 44431, 30: 55569}, + ("Category Two Pair", 6, 2): {0: 17183, 30: 82817}, + ("Category Two Pair", 6, 3): {0: 6759, 30: 93241}, + ("Category Two Pair", 6, 4): {0: 2562, 30: 97438}, + ("Category Two Pair", 6, 5): {0: 948, 30: 99052}, + ("Category Two Pair", 6, 6): {0: 375, 30: 99625}, + ("Category Two Pair", 6, 7): {0: 138, 30: 99862}, + ("Category Two Pair", 6, 8): {0: 57, 30: 99943}, + ("Category Two Pair", 7, 1): {0: 19888, 30: 80112}, + ("Category Two Pair", 7, 2): {0: 3935, 30: 96065}, + ("Category Two Pair", 7, 3): {0: 801, 30: 99199}, + ("Category Two Pair", 7, 4): {0: 175, 30: 99825}, + ("Category Two Pair", 7, 5): {0: 31, 30: 99969}, + ("Category Two Pair", 7, 6): {0: 7, 30: 99993}, + ("Category Two Pair", 7, 7): {0: 2, 30: 99998}, + ("Category Two Pair", 7, 8): {30: 100000}, + ("Category Two Pair", 8, 1): {0: 6791, 30: 93209}, + ("Category Two Pair", 8, 2): {0: 588, 30: 99412}, + ("Category Two Pair", 8, 3): {0: 61, 30: 99939}, + ("Category Two Pair", 8, 4): {0: 6, 30: 99994}, + ("Category Two Pair", 8, 5): {30: 100000}, + ("Category Two Pair", 8, 6): {30: 100000}, + ("Category Two Pair", 8, 7): {30: 100000}, + ("Category Two Pair", 8, 8): {30: 100000}, + ("Category 2-1-2 Consecutive", 1, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 1, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 2, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 3, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 1): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 2): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 3): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 4): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 5): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 6): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 7): {0: 100000}, + ("Category 2-1-2 Consecutive", 4, 8): {0: 100000}, + ("Category 2-1-2 Consecutive", 5, 1): {0: 98403, 40: 1597}, + ("Category 2-1-2 Consecutive", 5, 2): {0: 90651, 40: 9349}, + ("Category 2-1-2 Consecutive", 5, 3): {0: 80100, 40: 19900}, + ("Category 2-1-2 Consecutive", 5, 4): {0: 69131, 40: 30869}, + ("Category 2-1-2 Consecutive", 5, 5): {0: 58252, 40: 41748}, + ("Category 2-1-2 Consecutive", 5, 6): {0: 49405, 40: 50595}, + ("Category 2-1-2 Consecutive", 5, 7): {0: 41585, 40: 58415}, + ("Category 2-1-2 Consecutive", 5, 8): {0: 34952, 40: 65048}, + ("Category 2-1-2 Consecutive", 6, 1): {0: 93465, 40: 6535}, + ("Category 2-1-2 Consecutive", 6, 2): {0: 73416, 40: 26584}, + ("Category 2-1-2 Consecutive", 6, 3): {0: 54041, 40: 45959}, + ("Category 2-1-2 Consecutive", 6, 4): {0: 38535, 40: 61465}, + ("Category 2-1-2 Consecutive", 6, 5): {0: 27366, 40: 72634}, + ("Category 2-1-2 Consecutive", 6, 6): {0: 18924, 40: 81076}, + ("Category 2-1-2 Consecutive", 6, 7): {0: 13387, 40: 86613}, + ("Category 2-1-2 Consecutive", 6, 8): {0: 9134, 40: 90866}, + ("Category 2-1-2 Consecutive", 7, 1): {0: 84168, 40: 15832}, + ("Category 2-1-2 Consecutive", 7, 2): {0: 52659, 40: 47341}, + ("Category 2-1-2 Consecutive", 7, 3): {0: 30435, 40: 69565}, + ("Category 2-1-2 Consecutive", 7, 4): {0: 17477, 40: 82523}, + ("Category 2-1-2 Consecutive", 7, 5): {0: 9782, 40: 90218}, + ("Category 2-1-2 Consecutive", 7, 6): {0: 5316, 40: 94684}, + ("Category 2-1-2 Consecutive", 7, 7): {0: 2995, 40: 97005}, + ("Category 2-1-2 Consecutive", 7, 8): {0: 1689, 40: 98311}, + ("Category 2-1-2 Consecutive", 8, 1): {0: 71089, 40: 28911}, + ("Category 2-1-2 Consecutive", 8, 2): {0: 33784, 40: 66216}, + ("Category 2-1-2 Consecutive", 8, 3): {0: 14820, 40: 85180}, + ("Category 2-1-2 Consecutive", 8, 4): {0: 6265, 40: 93735}, + ("Category 2-1-2 Consecutive", 8, 5): {0: 2600, 40: 97400}, + ("Category 2-1-2 Consecutive", 8, 6): {0: 1155, 40: 98845}, + ("Category 2-1-2 Consecutive", 8, 7): {0: 487, 40: 99513}, + ("Category 2-1-2 Consecutive", 8, 8): {0: 190, 40: 99810}, + ("Category Five Distinct Dice", 1, 1): {0: 100000}, + ("Category Five Distinct Dice", 1, 2): {0: 100000}, + ("Category Five Distinct Dice", 1, 3): {0: 100000}, + ("Category Five Distinct Dice", 1, 4): {0: 100000}, + ("Category Five Distinct Dice", 1, 5): {0: 100000}, + ("Category Five Distinct Dice", 1, 6): {0: 100000}, + ("Category Five Distinct Dice", 1, 7): {0: 100000}, + ("Category Five Distinct Dice", 1, 8): {0: 100000}, + ("Category Five Distinct Dice", 2, 1): {0: 100000}, + ("Category Five Distinct Dice", 2, 2): {0: 100000}, + ("Category Five Distinct Dice", 2, 3): {0: 100000}, + ("Category Five Distinct Dice", 2, 4): {0: 100000}, + ("Category Five Distinct Dice", 2, 5): {0: 100000}, + ("Category Five Distinct Dice", 2, 6): {0: 100000}, + ("Category Five Distinct Dice", 2, 7): {0: 100000}, + ("Category Five Distinct Dice", 2, 8): {0: 100000}, + ("Category Five Distinct Dice", 3, 1): {0: 100000}, + ("Category Five Distinct Dice", 3, 2): {0: 100000}, + ("Category Five Distinct Dice", 3, 3): {0: 100000}, + ("Category Five Distinct Dice", 3, 4): {0: 100000}, + ("Category Five Distinct Dice", 3, 5): {0: 100000}, + ("Category Five Distinct Dice", 3, 6): {0: 100000}, + ("Category Five Distinct Dice", 3, 7): {0: 100000}, + ("Category Five Distinct Dice", 3, 8): {0: 100000}, + ("Category Five Distinct Dice", 4, 1): {0: 100000}, + ("Category Five Distinct Dice", 4, 2): {0: 100000}, + ("Category Five Distinct Dice", 4, 3): {0: 100000}, + ("Category Five Distinct Dice", 4, 4): {0: 100000}, + ("Category Five Distinct Dice", 4, 5): {0: 100000}, + ("Category Five Distinct Dice", 4, 6): {0: 100000}, + ("Category Five Distinct Dice", 4, 7): {0: 100000}, + ("Category Five Distinct Dice", 4, 8): {0: 100000}, + ("Category Five Distinct Dice", 5, 1): {0: 90907, 25: 9093}, + ("Category Five Distinct Dice", 5, 2): {0: 68020, 25: 31980}, + ("Category Five Distinct Dice", 5, 3): {0: 47692, 25: 52308}, + ("Category Five Distinct Dice", 5, 4): {0: 32383, 25: 67617}, + ("Category Five Distinct Dice", 5, 5): {0: 21631, 25: 78369}, + ("Category Five Distinct Dice", 5, 6): {0: 14366, 25: 85634}, + ("Category Five Distinct Dice", 5, 7): {0: 9568, 25: 90432}, + ("Category Five Distinct Dice", 5, 8): {0: 6360, 25: 93640}, + ("Category Five Distinct Dice", 6, 1): {0: 75051, 25: 24949}, + ("Category Five Distinct Dice", 6, 2): {0: 38409, 25: 61591}, + ("Category Five Distinct Dice", 6, 3): {0: 17505, 25: 82495}, + ("Category Five Distinct Dice", 6, 4): {0: 7862, 25: 92138}, + ("Category Five Distinct Dice", 6, 5): {0: 3538, 25: 96462}, + ("Category Five Distinct Dice", 6, 6): {0: 1645, 25: 98355}, + ("Category Five Distinct Dice", 6, 7): {0: 714, 25: 99286}, + ("Category Five Distinct Dice", 6, 8): {0: 341, 25: 99659}, + ("Category Five Distinct Dice", 7, 1): {0: 58588, 25: 41412}, + ("Category Five Distinct Dice", 7, 2): {0: 19487, 25: 80513}, + ("Category Five Distinct Dice", 7, 3): {0: 6043, 25: 93957}, + ("Category Five Distinct Dice", 7, 4): {0: 1799, 25: 98201}, + ("Category Five Distinct Dice", 7, 5): {0: 544, 25: 99456}, + ("Category Five Distinct Dice", 7, 6): {0: 169, 25: 99831}, + ("Category Five Distinct Dice", 7, 7): {0: 59, 25: 99941}, + ("Category Five Distinct Dice", 7, 8): {0: 11, 25: 99989}, + ("Category Five Distinct Dice", 8, 1): {0: 43586, 25: 56414}, + ("Category Five Distinct Dice", 8, 2): {0: 9615, 25: 90385}, + ("Category Five Distinct Dice", 8, 3): {0: 1944, 25: 98056}, + ("Category Five Distinct Dice", 8, 4): {0: 383, 25: 99617}, + ("Category Five Distinct Dice", 8, 5): {0: 77, 25: 99923}, + ("Category Five Distinct Dice", 8, 6): {0: 18, 25: 99982}, + ("Category Five Distinct Dice", 8, 7): {0: 3, 25: 99997}, + ("Category Five Distinct Dice", 8, 8): {0: 2, 25: 99998}, + ("Category 4&5 Full House", 1, 1): {0: 100000}, + ("Category 4&5 Full House", 1, 2): {0: 100000}, + ("Category 4&5 Full House", 1, 3): {0: 100000}, + ("Category 4&5 Full House", 1, 4): {0: 100000}, + ("Category 4&5 Full House", 1, 5): {0: 100000}, + ("Category 4&5 Full House", 1, 6): {0: 100000}, + ("Category 4&5 Full House", 1, 7): {0: 100000}, + ("Category 4&5 Full House", 1, 8): {0: 100000}, + ("Category 4&5 Full House", 2, 1): {0: 100000}, + ("Category 4&5 Full House", 2, 2): {0: 100000}, + ("Category 4&5 Full House", 2, 3): {0: 100000}, + ("Category 4&5 Full House", 2, 4): {0: 100000}, + ("Category 4&5 Full House", 2, 5): {0: 100000}, + ("Category 4&5 Full House", 2, 6): {0: 100000}, + ("Category 4&5 Full House", 2, 7): {0: 100000}, + ("Category 4&5 Full House", 2, 8): {0: 100000}, + ("Category 4&5 Full House", 3, 1): {0: 100000}, + ("Category 4&5 Full House", 3, 2): {0: 100000}, + ("Category 4&5 Full House", 3, 3): {0: 100000}, + ("Category 4&5 Full House", 3, 4): {0: 100000}, + ("Category 4&5 Full House", 3, 5): {0: 100000}, + ("Category 4&5 Full House", 3, 6): {0: 100000}, + ("Category 4&5 Full House", 3, 7): {0: 100000}, + ("Category 4&5 Full House", 3, 8): {0: 100000}, + ("Category 4&5 Full House", 4, 1): {0: 100000}, + ("Category 4&5 Full House", 4, 2): {0: 100000}, + ("Category 4&5 Full House", 4, 3): {0: 100000}, + ("Category 4&5 Full House", 4, 4): {0: 100000}, + ("Category 4&5 Full House", 4, 5): {0: 100000}, + ("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: 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}, + ("Category 4&5 Full House", 5, 5): {0: 65797, 50: 34203}, + ("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: 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}, + ("Category 4&5 Full House", 6, 5): {0: 35756, 50: 64244}, + ("Category 4&5 Full House", 6, 6): {0: 24698, 50: 75302}, + ("Category 4&5 Full House", 6, 7): {0: 17145, 50: 82855}, + ("Category 4&5 Full House", 6, 8): {0: 11846, 50: 88154}, + ("Category 4&5 Full House", 7, 1): {0: 97090, 50: 2910}, + ("Category 4&5 Full House", 7, 2): {0: 77440, 50: 22560}, + ("Category 4&5 Full House", 7, 3): {0: 51372, 50: 48628}, + ("Category 4&5 Full House", 7, 4): {0: 30566, 50: 69434}, + ("Category 4&5 Full House", 7, 5): {0: 17866, 50: 82134}, + ("Category 4&5 Full House", 7, 6): {0: 10521, 50: 89479}, + ("Category 4&5 Full House", 7, 7): {0: 6204, 50: 93796}, + ("Category 4&5 Full House", 7, 8): {0: 3670, 50: 96330}, + ("Category 4&5 Full House", 8, 1): {0: 94172, 50: 5828}, + ("Category 4&5 Full House", 8, 2): {0: 64693, 50: 35307}, + ("Category 4&5 Full House", 8, 3): {0: 35293, 50: 64707}, + ("Category 4&5 Full House", 8, 4): {0: 17749, 50: 82251}, + ("Category 4&5 Full House", 8, 5): {0: 8740, 50: 91260}, + ("Category 4&5 Full House", 8, 6): {0: 4550, 50: 95450}, + ("Category 4&5 Full House", 8, 7): {0: 2218, 50: 97782}, + ("Category 4&5 Full House", 8, 8): {0: 1084, 50: 98916}, +} diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py new file mode 100644 index 000000000000..7efb8f94187c --- /dev/null +++ b/worlds/yachtdice/__init__.py @@ -0,0 +1,530 @@ +import math +from typing import Dict + +from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region, Tutorial + +from worlds.AutoWorld import WebWorld, World + +from .Items import YachtDiceItem, item_groups, item_table +from .Locations import YachtDiceLocation, all_locations, ini_locations +from .Options import ( + AddExtraPoints, + AddStoryChapters, + GameDifficulty, + MinimalNumberOfDiceAndRolls, + MinimizeExtraItems, + PointsSize, + YachtDiceOptions, + yd_option_groups, +) +from .Rules import dice_simulation_fill_pool, set_yacht_completion_rules, set_yacht_rules + + +class YachtDiceWeb(WebWorld): + tutorials = [ + Tutorial( + "Multiworld Setup Guide", + "A guide to setting up Yacht Dice. This guide covers single-player, multiworld, and website.", + "English", + "setup_en.md", + "setup/en", + ["Spineraks"], + ) + ] + + option_groups = yd_option_groups + + +class YachtDiceWorld(World): + """ + Yacht Dice is a straightforward game, custom-made for Archipelago, + where you cast your dice to chart a course for high scores, + unlocking valuable treasures along the way. + Discover more dice, extra rolls, multipliers, + and unlockable categories to navigate the depths of the game. + Roll your way to victory by reaching the target score! + """ + + game: str = "Yacht Dice" + options_dataclass = YachtDiceOptions + + web = YachtDiceWeb() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + + location_name_to_id = {name: data.id for name, data in all_locations.items()} + + item_name_groups = item_groups + + ap_world_version = "2.1.4" + + def _get_yachtdice_data(self): + return { + # "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), + "seed_name": self.multiworld.seed_name, + "player_name": self.multiworld.get_player_name(self.player), + "player_id": self.player, + "race": self.multiworld.is_race, + } + + def generate_early(self): + """ + In generate early, we fill the item-pool, then determine the number of locations, and add filler items. + """ + self.itempool = [] + self.precollected = [] + + # number of dice and rolls in the pull + opt_dice_and_rolls = self.options.minimal_number_of_dice_and_rolls + + if opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_5_dice_and_3_rolls: + num_of_dice = 5 + num_of_rolls = 3 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_5_dice_and_5_rolls: + num_of_dice = 5 + num_of_rolls = 5 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_6_dice_and_4_rolls: + num_of_dice = 6 + num_of_rolls = 4 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_7_dice_and_3_rolls: + num_of_dice = 7 + num_of_rolls = 3 + elif opt_dice_and_rolls == MinimalNumberOfDiceAndRolls.option_8_dice_and_2_rolls: + num_of_dice = 8 + num_of_rolls = 2 + else: + raise Exception(f"[Yacht Dice] Unknown MinimalNumberOfDiceAndRolls options {opt_dice_and_rolls}") + + # amount of dice and roll fragments needed to get a dice or roll + self.frags_per_dice = self.options.number_of_dice_fragments_per_dice.value + self.frags_per_roll = self.options.number_of_roll_fragments_per_roll.value + + if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please: + self.frags_per_dice = min(self.frags_per_dice, 2) + self.frags_per_roll = min(self.frags_per_roll, 2) + + # set difficulty + diff_value = self.options.game_difficulty + if diff_value == GameDifficulty.option_easy: + self.difficulty = 1 + elif diff_value == GameDifficulty.option_medium: + self.difficulty = 2 + elif diff_value == GameDifficulty.option_hard: + self.difficulty = 3 + elif diff_value == GameDifficulty.option_extreme: + self.difficulty = 4 + else: + raise Exception(f"[Yacht Dice] Unknown GameDifficulty options {diff_value}") + + # Create a list with the specified number of 1s + num_ones = self.options.alternative_categories.value + categorylist = [1] * num_ones + [0] * (16 - num_ones) + + # Shuffle the list to randomize the order + self.random.shuffle(categorylist) + + # A list of all possible categories. + # Every entry in the list has two categories, one 'default' category and one 'alt'. + # You get either of the two for every entry, so a total of 16 unique categories. + all_categories = [ + ["Category Choice", "Category Double Threes and Fours"], + ["Category Inverse Choice", "Category Quadruple Ones and Twos"], + ["Category Ones", "Category Distincts"], + ["Category Twos", "Category Two times Ones"], + ["Category Threes", "Category Half of Sixes"], + ["Category Fours", "Category Twos and Threes"], + ["Category Fives", "Category Sum of Odds"], + ["Category Sixes", "Category Sum of Evens"], + ["Category Pair", "Category Micro Straight"], + ["Category Three of a Kind", "Category Three Odds"], + ["Category Four of a Kind", "Category 1-2-1 Consecutive"], + ["Category Tiny Straight", "Category Three Distinct Dice"], + ["Category Small Straight", "Category Two Pair"], + ["Category Large Straight", "Category 2-1-2 Consecutive"], + ["Category Full House", "Category Five Distinct Dice"], + ["Category Yacht", "Category 4&5 Full House"], + ] + + # categories used in this game. + self.possible_categories = [] + + for index, cats in enumerate(all_categories): + self.possible_categories.append(cats[categorylist[index]]) + + # Add Choice and Inverse choice (or their alts) to the precollected list. + if index == 0 or index == 1: + self.precollected.append(cats[categorylist[index]]) + else: + self.itempool.append(cats[categorylist[index]]) + + # Also start with one Roll and one Dice + self.precollected.append("Dice") + num_of_dice_to_add = num_of_dice - 1 + self.precollected.append("Roll") + num_of_rolls_to_add = num_of_rolls - 1 + + self.skip_early_locations = False + if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please: + self.precollected.append("Dice") + num_of_dice_to_add -= 1 + self.precollected.append("Roll") + num_of_rolls_to_add -= 1 + self.skip_early_locations = True + + if num_of_dice_to_add > 0: + self.itempool.append("Dice") + num_of_dice_to_add -= 1 + if num_of_rolls_to_add > 0: + self.itempool.append("Roll") + num_of_rolls_to_add -= 1 + + # if one fragment per dice, just add "Dice" objects + if num_of_dice_to_add > 0: + if self.frags_per_dice == 1: + self.itempool += ["Dice"] * num_of_dice_to_add # minus one because one is in start inventory + else: + self.itempool += ["Dice Fragment"] * (self.frags_per_dice * num_of_dice_to_add) + + # if one fragment per roll, just add "Roll" objects + if num_of_rolls_to_add > 0: + 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 += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add) + + already_items = len(self.itempool) + + # Yacht Dice needs extra filler items so it doesn't get stuck in generation. + # For now, we calculate the number of extra items we'll need later. + if self.options.minimize_extra_items == MinimizeExtraItems.option_yes_please: + extra_percentage = max(0.1, 0.8 - self.multiworld.players / 10) + elif self.options.minimize_extra_items == MinimizeExtraItems.option_no_dont: + extra_percentage = 0.72 + else: + raise Exception(f"[Yacht Dice] Unknown MinimizeExtraItems options {self.options.minimize_extra_items}") + extra_locations_needed = max(10, math.ceil(already_items * extra_percentage)) + + # max score is the value of the last check. Goal score is the score needed to 'finish' the game + self.max_score = self.options.score_for_last_check.value + self.goal_score = min(self.max_score, self.options.score_for_goal.value) + + # Yacht Dice adds items into the pool until a score of at least 1000 is reached. + # the yaml contains weights, which determine how likely it is that specific items get added. + # If all weights are 0, some of them will be made to be non-zero later. + weights: Dict[str, float] = { + "Dice": self.options.weight_of_dice.value, + "Roll": self.options.weight_of_roll.value, + "Fixed Score Multiplier": self.options.weight_of_fixed_score_multiplier.value, + "Step Score Multiplier": self.options.weight_of_step_score_multiplier.value, + "Double category": self.options.weight_of_double_category.value, + "Points": self.options.weight_of_points.value, + } + + # if the player wants extra rolls or dice, fill the pool with fragments until close to an extra roll/dice + if weights["Dice"] > 0 and self.frags_per_dice > 1: + self.itempool += ["Dice Fragment"] * (self.frags_per_dice - 1) + if weights["Roll"] > 0 and self.frags_per_roll > 1: + self.itempool += ["Roll Fragment"] * (self.frags_per_roll - 1) + + # calibrate the weights, since the impact of each of the items is different + weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice + weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll + + 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: + weights["Dice"] = 0 # don't allow >=9 dice + roll_fragments_in_pool = all_items.count("Roll") * self.frags_per_roll + all_items.count("Roll Fragment") + if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll: + weights["Roll"] = 0 # don't allow >= 6 rolls + + # Don't allow too many extra points + 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: + weights["Fixed Score Multiplier"] = 1 + weights["Double category"] = 1 + 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 + which_item_to_add = self.random.choices(list(weights.keys()), weights=list(weights.values()))[0] + + if which_item_to_add == "Dice": + weights["Dice"] /= 1 + self.frags_per_dice + return "Dice" if self.frags_per_dice == 1 else "Dice Fragment" + elif which_item_to_add == "Roll": + weights["Roll"] /= 1 + self.frags_per_roll + 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 + return "Fixed Score Multiplier" + elif which_item_to_add == "Step Score Multiplier": + weights["Step Score Multiplier"] /= 1.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. + # Prefer to add choice or number categories, because the other categories are too "all or nothing", + # which often don't give any points, until you get overpowered, and then they give all points. + cat_weights = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1] + weights["Double category"] /= 1.1 + return self.random.choices(self.possible_categories, weights=cat_weights)[0] + elif which_item_to_add == "Points": + score_dist = self.options.points_size + probs = {"1 Point": 1, "10 Points": 0, "100 Points": 0} + if score_dist == PointsSize.option_small: + probs = {"1 Point": 0.9, "10 Points": 0.1, "100 Points": 0} + elif score_dist == PointsSize.option_medium: + probs = {"1 Point": 0, "10 Points": 1, "100 Points": 0} + elif score_dist == PointsSize.option_large: + probs = {"1 Point": 0, "10 Points": 0.3, "100 Points": 0.7} + elif score_dist == PointsSize.option_mix: + probs = {"1 Point": 0.3, "10 Points": 0.4, "100 Points": 0.3} + else: + raise Exception(f"[Yacht Dice] Unknown PointsSize options {score_dist}") + choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0] + if choice == "1 Point": + weights["Points"] /= 1.01 + extra_points_added[0] += 1 + return "1 Point" + elif choice == "10 Points": + weights["Points"] /= 1.1 + extra_points_added[0] += 10 + return "10 Points" + elif choice == "100 Points": + weights["Points"] /= 2 + extra_points_added[0] += 100 + return "100 Points" + else: + raise Exception("Unknown point value (Yacht Dice)") + else: + raise Exception(f"Invalid index when adding new items in Yacht Dice: {which_item_to_add}") + + # 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, step_score_multipliers_added)) + + score_in_logic = dice_simulation_fill_pool( + self.itempool + self.precollected, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + self.player, + ) + + # if we overshoot, remove items until you get below 1000, then return the last removed item + if score_in_logic > 1000: + removed_item = "" + while score_in_logic > 1000: + removed_item = self.itempool.pop() + score_in_logic = dice_simulation_fill_pool( + self.itempool + self.precollected, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + self.player, + ) + self.itempool.append(removed_item) + 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, step_score_multipliers_added) + self.itempool.append(item_to_add) + if item_to_add == "1 Point": + score_in_logic += 1 + elif item_to_add == "10 Points": + score_in_logic += 10 + elif item_to_add == "100 Points": + score_in_logic += 100 + else: + score_in_logic = dice_simulation_fill_pool( + self.itempool + self.precollected, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + self.player, + ) + + # count the number of locations in the game. + already_items = len(self.itempool) + 1 # +1 because of Victory item + + # We need to add more filler/useful items if there are many items in the pool to guarantee successful generation + extra_locations_needed += (already_items - 45) // 15 + self.number_of_locations = already_items + extra_locations_needed + + # From here, we will count the number of items in the self.itempool, and add useful/filler items to the pool, + # making sure not to exceed the number of locations. + + # first, we flood the entire pool with extra points (useful), if that setting is chosen. + if self.options.add_bonus_points == AddExtraPoints.option_all_of_it: # all of the extra points + already_items = len(self.itempool) + 1 + self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 100) + + # second, we flood the entire pool with story chapters (filler), if that setting is chosen. + if self.options.add_story_chapters == AddStoryChapters.option_all_of_it: # all of the story chapters + already_items = len(self.itempool) + 1 + number_of_items = min(self.number_of_locations - already_items, 100) + number_of_items = (number_of_items // 10) * 10 # story chapters always come in multiples of 10 + self.itempool += ["Story Chapter"] * number_of_items + + # add some extra points (useful) + if self.options.add_bonus_points == AddExtraPoints.option_sure: # add extra points if wanted + already_items = len(self.itempool) + 1 + self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10) + + # add some story chapters (filler) + if self.options.add_story_chapters == AddStoryChapters.option_sure: # add extra points if wanted + already_items = len(self.itempool) + 1 + if self.number_of_locations - already_items >= 10: + self.itempool += ["Story Chapter"] * 10 + + # add some more extra points if there is still room + if self.options.add_bonus_points == AddExtraPoints.option_sure: + already_items = len(self.itempool) + 1 + self.itempool += ["Bonus Point"] * min(self.number_of_locations - already_items, 10) + + # add some encouragements filler-items if there is still room + already_items = len(self.itempool) + 1 + self.itempool += ["Encouragement"] * min(self.number_of_locations - already_items, 5) + + # add some fun facts filler-items if there is still room + already_items = len(self.itempool) + 1 + self.itempool += ["Fun Fact"] * min(self.number_of_locations - already_items, 5) + + # finally, add some "Good RNG" and "Bad RNG" items to complete the item pool + # these items are filler and do not do anything. + + # probability of Good and Bad rng, based on difficulty for fun :) + + p = 1.1 - 0.25 * self.difficulty + already_items = len(self.itempool) + 1 + self.itempool += self.random.choices( + ["Good RNG", "Bad RNG"], weights=[p, 1 - p], k=self.number_of_locations - already_items + ) + + # we are done adding items. Now because of the last step, number of items should be number of locations + already_items = len(self.itempool) + 1 + if already_items != self.number_of_locations: + raise Exception( + f"[Yacht Dice] Number in self.itempool is not number of locations " + f"{already_items} {self.number_of_locations}." + ) + + # add precollected items using push_precollected. Items in self.itempool get created in create_items + for item in self.precollected: + self.multiworld.push_precollected(self.create_item(item)) + + # make sure one dice and one roll is early, so that you will have 2 dice and 2 rolls soon + self.multiworld.early_items[self.player]["Dice"] = 1 + self.multiworld.early_items[self.player]["Roll"] = 1 + + def create_items(self): + self.multiworld.itempool += [self.create_item(name) for name in self.itempool] + + def create_regions(self): + # call the ini_locations function, that generates locations based on the inputs. + location_table = ini_locations( + self.goal_score, + self.max_score, + self.number_of_locations, + self.difficulty, + self.skip_early_locations, + self.multiworld.players, + ) + + # simple menu-board construction + menu = Region("Menu", self.player, self.multiworld) + board = Region("Board", self.player, self.multiworld) + + # add locations to board, one for every location in the location_table + board.locations = [ + YachtDiceLocation(self.player, loc_name, loc_data.score, loc_data.id, board) + for loc_name, loc_data in location_table.items() + if loc_data.region == board.name + ] + + # 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).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): + """ + set rules per location, and add the rule for beating the game + """ + set_yacht_rules( + self.multiworld, + self.player, + self.frags_per_dice, + self.frags_per_roll, + self.possible_categories, + self.difficulty, + ) + set_yacht_completion_rules(self.multiworld, self.player) + + def fill_slot_data(self): + """ + make slot data, which consists of yachtdice_data, options, and some other variables. + """ + yacht_dice_data = self._get_yachtdice_data() + yacht_dice_options = self.options.as_dict( + "game_difficulty", + "score_for_last_check", + "score_for_goal", + "number_of_dice_fragments_per_dice", + "number_of_roll_fragments_per_roll", + "which_story", + "allow_manual_input", + ) + slot_data = {**yacht_dice_data, **yacht_dice_options} # combine the two + slot_data["number_of_dice_fragments_per_dice"] = self.frags_per_dice + slot_data["number_of_roll_fragments_per_roll"] = self.frags_per_roll + slot_data["goal_score"] = self.goal_score + slot_data["last_check_score"] = self.max_score + slot_data["allowed_categories"] = self.possible_categories + slot_data["ap_world_version"] = self.ap_world_version + return slot_data + + def create_item(self, name: str) -> Item: + item_data = item_table[name] + item = YachtDiceItem(name, item_data.classification, item_data.code, self.player) + return item + + # We overwrite these function to monitor when states have changed. See also dice_simulation in Rules.py + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change: + state.prog_items[self.player]["state_is_fresh"] = 0 + + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change: + state.prog_items[self.player]["state_is_fresh"] = 0 + + return change diff --git a/worlds/yachtdice/docs/en_Yacht Dice.md b/worlds/yachtdice/docs/en_Yacht Dice.md new file mode 100644 index 000000000000..c671dcee50b8 --- /dev/null +++ b/worlds/yachtdice/docs/en_Yacht Dice.md @@ -0,0 +1,15 @@ +# Yacht Dice + +Welcome to Yacht Dice, the ultimate dice-rolling adventure in Archipelago! Cast your dice, chase high scores, and unlock valuable treasures. Discover new dice, extra rolls, multipliers, and special scoring categories to enhance your game. Roll your way to victory by reaching the target score! + +## Understanding Location Checks +In Yacht Dice, location checks happen when you hit certain scores for the first time. The target score for your next location check is always displayed in the game. + +## Items and Their Effects +When you receive an item, it could be extra dice, extra rolls, score multipliers, or new scoring categories. These boosts help you sail towards higher scores and more loot. Other items include extra points, lore, and fun facts to enrich your journey. + +## Winning the Game +Victory in Yacht Dice is all about reaching the target score. You can set your own target score, which is displayed on the website. Once you hit it, you've conquered the game! + +## How to Access Options +Need to tweak your game? Head over to the [player options page](../player-options) for all your configuration options and to export your config file. diff --git a/worlds/yachtdice/docs/setup_en.md b/worlds/yachtdice/docs/setup_en.md new file mode 100644 index 000000000000..f6c15af2b63c --- /dev/null +++ b/worlds/yachtdice/docs/setup_en.md @@ -0,0 +1,15 @@ +# Yacht Dice Randomizer Setup Guide + +## Required Software + +- A browser (you are probably using one right now!). + +## Playing the game +Open the Yacht Dice website. There are two options: +- Cruise over to the [Yacht Dice Website](https://yacht-dice-ap.netlify.app/). This is the easiest option. If the website is unavailable, use the next option. +- Download the latest release from [Yacht Dice Release](https://github.com/spinerak/ArchipelagoYachtDice/releases/latest) and unzip the Website.zip. Then open index.html in your browser. + +Press Archipelago, and after logging in, you are good to go. The website has a built-in client, where you can chat and send commands. +Both options also have a "Solo play" mode to try out the game without having to generate a game first. + +For more information on generating Archipelago games and connecting to servers, please see the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). \ No newline at end of file diff --git a/worlds/yugioh06/__init__.py b/worlds/yugioh06/__init__.py index 1cf44f090fed..9070683f33d5 100644 --- a/worlds/yugioh06/__init__.py +++ b/worlds/yugioh06/__init__.py @@ -1,6 +1,6 @@ import os import pkgutil -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Set import settings from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial @@ -17,12 +17,14 @@ draft_opponents, excluded_items, item_to_index, - tier_1_opponents, useful, + tier_1_opponents, + tier_2_opponents, + tier_3_opponents, + tier_4_opponents, + tier_5_opponents, ) -from .items import ( - challenges as challenges, -) +from .items import challenges as challenges from .locations import ( Bonuses, Campaign_Opponents, @@ -50,7 +52,7 @@ class Yugioh06Web(WebWorld): theme = "stone" setup = Tutorial( - "Multiworld Setup Tutorial", + "Multiworld Setup Guide", "A guide to setting up Yu-Gi-Oh! - Ultimate Masters Edition - World Championship Tournament 2006 " "for Archipelago on your computer.", "English", @@ -109,9 +111,17 @@ class Yugioh06World(World): for k, v in Required_Cards.items(): location_name_to_id[k] = v + start_id - item_name_groups = { - "Core Booster": core_booster, - "Campaign Boss Beaten": ["Tier 1 Beaten", "Tier 2 Beaten", "Tier 3 Beaten", "Tier 4 Beaten", "Tier 5 Beaten"], + item_name_groups: Dict[str, Set[str]] = { + "Core Booster": set(core_booster), + "Campaign Boss Beaten": {"Tier 1 Beaten", "Tier 2 Beaten", "Tier 3 Beaten", "Tier 4 Beaten", "Tier 5 Beaten"}, + "Challenge": set(challenges), + "Tier 1 Opponent": set(tier_1_opponents), + "Tier 2 Opponent": set(tier_2_opponents), + "Tier 3 Opponent": set(tier_3_opponents), + "Tier 4 Opponent": set(tier_4_opponents), + "Tier 5 Opponent": set(tier_5_opponents), + "Campaign Opponent": set(tier_1_opponents + tier_2_opponents + tier_3_opponents + + tier_4_opponents + tier_5_opponents) } removed_challenges: List[str] @@ -430,7 +440,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/boosterpacks.py b/worlds/yugioh06/boosterpacks.py index f6f4ec7732c3..645977d28def 100644 --- a/worlds/yugioh06/boosterpacks.py +++ b/worlds/yugioh06/boosterpacks.py @@ -1,13 +1,13 @@ -from typing import Dict, Set +from typing import Dict, List -booster_contents: Dict[str, Set[str]] = { - "LEGEND OF B.E.W.D.": { +booster_contents: Dict[str, List[str]] = { + "LEGEND OF B.E.W.D.": [ "Exodia", "Dark Magician", "Polymerization", "Skull Servant" - }, - "METAL RAIDERS": { + ], + "METAL RAIDERS": [ "Petit Moth", "Cocoon of Evolution", "Time Wizard", @@ -30,8 +30,8 @@ "Solemn Judgment", "Dream Clown", "Heavy Storm" - }, - "PHARAOH'S SERVANT": { + ], + "PHARAOH'S SERVANT": [ "Beast of Talwar", "Jinzo", "Gearfried the Iron Knight", @@ -43,8 +43,8 @@ "The Shallow Grave", "Nobleman of Crossout", "Magic Drain" - }, - "PHARAONIC GUARDIAN": { + ], + "PHARAONIC GUARDIAN": [ "Don Zaloog", "Reasoning", "Dark Snake Syndrome", @@ -71,8 +71,8 @@ "Book of Taiyou", "Dust Tornado", "Raigeki Break" - }, - "SPELL RULER": { + ], + "SPELL RULER": [ "Ritual", "Messenger of Peace", "Megamorph", @@ -94,8 +94,8 @@ "Senju of the Thousand Hands", "Sonic Bird", "Mystical Space Typhoon" - }, - "LABYRINTH OF NIGHTMARE": { + ], + "LABYRINTH OF NIGHTMARE": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -119,8 +119,8 @@ "United We Stand", "Earthbound Spirit", "The Masked Beast" - }, - "LEGACY OF DARKNESS": { + ], + "LEGACY OF DARKNESS": [ "Last Turn", "Yata-Garasu", "Opticlops", @@ -143,8 +143,8 @@ "Maharaghi", "Susa Soldier", "Emergency Provisions", - }, - "MAGICIAN'S FORCE": { + ], + "MAGICIAN'S FORCE": [ "Huge Revolution", "Oppressed People", "United Resistance", @@ -185,8 +185,8 @@ "Royal Magical Library", "Spell Shield Type-8", "Tribute Doll", - }, - "DARK CRISIS": { + ], + "DARK CRISIS": [ "Final Countdown", "Ojama Green", "Dark Scorpion Combination", @@ -213,8 +213,8 @@ "Spell Reproduction", "Contract with the Abyss", "Dark Master - Zorc" - }, - "INVASION OF CHAOS": { + ], + "INVASION OF CHAOS": [ "Ojama Delta Hurricane", "Ojama Yellow", "Ojama Black", @@ -241,8 +241,8 @@ "Cursed Seal of the Forbidden Spell", "Stray Lambs", "Manju of the Ten Thousand Hands" - }, - "ANCIENT SANCTUARY": { + ], + "ANCIENT SANCTUARY": [ "Monster Gate", "Wall of Revealing Light", "Mystik Wok", @@ -255,8 +255,8 @@ "King of the Swamp", "Enemy Controller", "Enchanting Fitting Room" - }, - "SOUL OF THE DUELIST": { + ], + "SOUL OF THE DUELIST": [ "Ninja Grandmaster Sasuke", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -272,8 +272,8 @@ "Level Up!", "Howling Insect", "Mobius the Frost Monarch" - }, - "RISE OF DESTINY": { + ], + "RISE OF DESTINY": [ "Homunculus the Alchemic Being", "Thestalos the Firestorm Monarch", "Roc from the Valley of Haze", @@ -283,8 +283,8 @@ "Ultimate Insect Lv3", "Divine Wrath", "Serial Spell" - }, - "FLAMING ETERNITY": { + ], + "FLAMING ETERNITY": [ "Insect Knight", "Chiron the Mage", "Granmarg the Rock Monarch", @@ -297,8 +297,8 @@ "Golem Sentry", "Rescue Cat", "Blade Rabbit" - }, - "THE LOST MILLENIUM": { + ], + "THE LOST MILLENIUM": [ "Ritual", "Megarock Dragon", "D.D. Survivor", @@ -311,8 +311,8 @@ "Elemental Hero Thunder Giant", "Aussa the Earth Charmer", "Brain Control" - }, - "CYBERNETIC REVOLUTION": { + ], + "CYBERNETIC REVOLUTION": [ "Power Bond", "Cyber Dragon", "Cyber Twin Dragon", @@ -322,8 +322,8 @@ "Miracle Fusion", "Elemental Hero Bubbleman", "Jerry Beans Man" - }, - "ELEMENTAL ENERGY": { + ], + "ELEMENTAL ENERGY": [ "V-Tiger Jet", "W-Wing Catapult", "VW-Tiger Catapult", @@ -344,8 +344,8 @@ "Elemental Hero Bladedge", "Pot of Avarice", "B.E.S. Tetran" - }, - "SHADOW OF INFINITY": { + ], + "SHADOW OF INFINITY": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -357,8 +357,8 @@ "Gokipon", "Demise, King of Armageddon", "Anteatereatingant" - }, - "GAME GIFT COLLECTION": { + ], + "GAME GIFT COLLECTION": [ "Ritual", "Valkyrion the Magna Warrior", "Alpha the Magnet Warrior", @@ -383,8 +383,8 @@ "Card Destruction", "Dark Magic Ritual", "Calamity of the Wicked" - }, - "Special Gift Collection": { + ], + "Special Gift Collection": [ "Gate Guardian", "Scapegoat", "Gil Garth", @@ -398,8 +398,8 @@ "Curse of Vampire", "Elemental Hero Flame Wingman", "Magician of Black Chaos" - }, - "Fairy Collection": { + ], + "Fairy Collection": [ "Silpheed", "Dunames Dark Witch", "Hysteric Fairy", @@ -416,8 +416,8 @@ "Asura Priest", "Manju of the Ten Thousand Hands", "Senju of the Thousand Hands" - }, - "Dragon Collection": { + ], + "Dragon Collection": [ "Victory D.", "Chaos Emperor Dragon - Envoy of the End", "Kaiser Glider", @@ -434,16 +434,16 @@ "Troop Dragon", "Horus the Black Flame Dragon LV4", "Pitch-Dark Dragon" - }, - "Warrior Collection A": { + ], + "Warrior Collection A": [ "Gate Guardian", "Gearfried the Iron Knight", "Dimensional Warrior", "Command Knight", "The Last Warrior from Another Planet", "Dream Clown" - }, - "Warrior Collection B": { + ], + "Warrior Collection B": [ "Don Zaloog", "Dark Scorpion - Chick the Yellow", "Dark Scorpion - Meanae the Thorn", @@ -467,8 +467,8 @@ "Blade Knight", "Marauding Captain", "Toon Goblin Attack Force" - }, - "Fiend Collection A": { + ], + "Fiend Collection A": [ "Sangan", "Castle of Dark Illusions", "Barox", @@ -480,8 +480,8 @@ "Spear Cretin", "Versago the Destroyer", "Toon Summoned Skull" - }, - "Fiend Collection B": { + ], + "Fiend Collection B": [ "Raviel, Lord of Phantasms", "Yata-Garasu", "Helpoemer", @@ -505,15 +505,15 @@ "Jowls of Dark Demise", "D. D. Trainer", "Earthbound Spirit" - }, - "Machine Collection A": { + ], + "Machine Collection A": [ "Cyber-Stein", "Mechanicalchaser", "Jinzo", "UFO Turtle", "Cyber-Tech Alligator" - }, - "Machine Collection B": { + ], + "Machine Collection B": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -531,8 +531,8 @@ "Red Gadget", "Yellow Gadget", "B.E.S. Tetran" - }, - "Spellcaster Collection A": { + ], + "Spellcaster Collection A": [ "Exodia", "Dark Sage", "Dark Magician", @@ -544,8 +544,8 @@ "Injection Fairy Lily", "Cosmo Queen", "Magician of Black Chaos" - }, - "Spellcaster Collection B": { + ], + "Spellcaster Collection B": [ "Jowgen the Spiritualist", "Tsukuyomi", "Manticore of Darkness", @@ -574,8 +574,8 @@ "Royal Magical Library", "Aussa the Earth Charmer", - }, - "Zombie Collection": { + ], + "Zombie Collection": [ "Skull Servant", "Regenerating Mummy", "Ryu Kokki", @@ -590,8 +590,8 @@ "Des Lacooda", "Wandering Mummy", "Royal Keeper" - }, - "Special Monsters A": { + ], + "Special Monsters A": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -626,8 +626,8 @@ "Fushi No Tori", "Maharaghi", "Susa Soldier" - }, - "Special Monsters B": { + ], + "Special Monsters B": [ "Polymerization", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -656,8 +656,8 @@ "Level Up!", "Ultimate Insect Lv3", "Ultimate Insect Lv5" - }, - "Reverse Collection": { + ], + "Reverse Collection": [ "Magical Merchant", "Castle of Dark Illusions", "Magician of Faith", @@ -675,8 +675,8 @@ "Spear Cretin", "Nobleman of Crossout", "Aussa the Earth Charmer" - }, - "LP Recovery Collection": { + ], + "LP Recovery Collection": [ "Mystik Wok", "Poison of the Old Man", "Hysteric Fairy", @@ -691,8 +691,8 @@ "Elemental Hero Steam Healer", "Fushi No Tori", "Emergency Provisions" - }, - "Special Summon Collection A": { + ], + "Special Summon Collection A": [ "Perfectly Ultimate Great Moth", "Dark Sage", "Polymerization", @@ -726,8 +726,8 @@ "Morphing Jar #2", "Spear Cretin", "Dark Magic Curtain" - }, - "Special Summon Collection B": { + ], + "Special Summon Collection B": [ "Monster Gate", "Chaos Emperor Dragon - Envoy of the End", "Ojama Trio", @@ -756,8 +756,8 @@ "Tribute Doll", "Enchanting Fitting Room", "Stray Lambs" - }, - "Special Summon Collection C": { + ], + "Special Summon Collection C": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -782,13 +782,13 @@ "Ultimate Insect Lv5", "Rescue Cat", "Anteatereatingant" - }, - "Equipment Collection": { + ], + "Equipment Collection": [ "Megamorph", "Cestus of Dagla", "United We Stand" - }, - "Continuous Spell/Trap A": { + ], + "Continuous Spell/Trap A": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -801,8 +801,8 @@ "Solemn Wishes", "Embodiment of Apophis", "Toon World" - }, - "Continuous Spell/Trap B": { + ], + "Continuous Spell/Trap B": [ "Hamon, Lord of Striking Thunder", "Uria, Lord of Searing Flames", "Wave-Motion Cannon", @@ -815,8 +815,8 @@ "Skull Zoma", "Pitch-Black Power Stone", "Metal Reflect Slime" - }, - "Quick/Counter Collection": { + ], + "Quick/Counter Collection": [ "Mystik Wok", "Poison of the Old Man", "Scapegoat", @@ -841,8 +841,8 @@ "Book of Moon", "Serial Spell", "Mystical Space Typhoon" - }, - "Direct Damage Collection": { + ], + "Direct Damage Collection": [ "Hamon, Lord of Striking Thunder", "Chaos Emperor Dragon - Envoy of the End", "Dark Snake Syndrome", @@ -868,8 +868,8 @@ "Jowls of Dark Demise", "Stealth Bird", "Elemental Hero Bladedge", - }, - "Direct Attack Collection": { + ], + "Direct Attack Collection": [ "Victory D.", "Dark Scorpion Combination", "Spirit Reaper", @@ -880,8 +880,8 @@ "Toon Mermaid", "Toon Summoned Skull", "Toon Dark Magician Girl" - }, - "Monster Destroy Collection": { + ], + "Monster Destroy Collection": [ "Hamon, Lord of Striking Thunder", "Inferno", "Ninja Grandmaster Sasuke", @@ -912,12 +912,12 @@ "Offerings to the Doomed", "Divine Wrath", "Dream Clown" - }, + ], } def get_booster_locations(booster: str) -> Dict[str, str]: return { f"{booster} {i}": content - for i, content in enumerate(booster_contents[booster]) + for i, content in enumerate(booster_contents[booster], 1) } diff --git a/worlds/yugioh06/items.py b/worlds/yugioh06/items.py index f0f877fd9f7b..0cfcf32992f2 100644 --- a/worlds/yugioh06/items.py +++ b/worlds/yugioh06/items.py @@ -183,6 +183,35 @@ "Campaign Tier 1 Column 5", ] +tier_2_opponents: List[str] = [ + "Campaign Tier 2 Column 1", + "Campaign Tier 2 Column 2", + "Campaign Tier 2 Column 3", + "Campaign Tier 2 Column 4", + "Campaign Tier 2 Column 5", +] + +tier_3_opponents: List[str] = [ + "Campaign Tier 3 Column 1", + "Campaign Tier 3 Column 2", + "Campaign Tier 3 Column 3", + "Campaign Tier 3 Column 4", + "Campaign Tier 3 Column 5", +] + +tier_4_opponents: List[str] = [ + "Campaign Tier 4 Column 1", + "Campaign Tier 4 Column 2", + "Campaign Tier 4 Column 3", + "Campaign Tier 4 Column 4", + "Campaign Tier 4 Column 5", +] + +tier_5_opponents: List[str] = [ + "Campaign Tier 5 Column 1", + "Campaign Tier 5 Column 2", +] + Banlist_Items: List[str] = [ "No Banlist", "Banlist September 2003", 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/yugioh06/structure_deck.py b/worlds/yugioh06/structure_deck.py index d58223f2e216..3559e7c5153e 100644 --- a/worlds/yugioh06/structure_deck.py +++ b/worlds/yugioh06/structure_deck.py @@ -1,7 +1,7 @@ -from typing import Dict, Set +from typing import Dict, List -structure_contents: Dict[str, Set] = { - "dragons_roar": { +structure_contents: Dict[str, List[str]] = { + "dragons_roar": [ "Luster Dragon", "Armed Dragon LV3", "Armed Dragon LV5", @@ -14,9 +14,9 @@ "Stamping Destruction", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "zombie_madness": { + "Mystical Space Typhoon" + ], + "zombie_madness": [ "Pyramid Turtle", "Regenerating Mummy", "Ryu Kokki", @@ -26,9 +26,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "blazing_destruction": { + "Mystical Space Typhoon" + ], + "blazing_destruction": [ "Inferno", "Solar Flare Dragon", "UFO Turtle", @@ -38,9 +38,9 @@ "Level Limit - Area B", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "fury_from_the_deep": { + "Mystical Space Typhoon" + ], + "fury_from_the_deep": [ "Mother Grizzly", "Water Beaters", "Gravity Bind", @@ -48,9 +48,9 @@ "Mobius the Frost Monarch", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "warriors_triumph": { + "Mystical Space Typhoon" + ], + "warriors_triumph": [ "Gearfried the Iron Knight", "D.D. Warrior Lady", "Marauding Captain", @@ -60,9 +60,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "spellcasters_judgement": { + "Mystical Space Typhoon" + ], + "spellcasters_judgement": [ "Dark Magician", "Apprentice Magician", "Breaker the Magical Warrior", @@ -70,14 +70,18 @@ "Skilled Dark Magician", "Tsukuyomi", "Magical Dimension", - "Mage PowerSpell-Counter Cards", + "Mage Power", + "Spell-Counter Cards", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "none": {}, + "Mystical Space Typhoon" + ], + "none": [], } def get_deck_content_locations(deck: str) -> Dict[str, str]: - return {f"{deck} {i}": content for i, content in enumerate(structure_contents[deck])} + return { + f"{deck} {i}": content + for i, content in enumerate(structure_contents[deck], 1) + } diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 205cc9ad6ba1..5a4e2bb48f18 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -3,16 +3,17 @@ import functools import settings import threading -import typing -from typing import Any, Dict, List, Set, Tuple, Optional +from typing import Any, ClassVar import os import logging +from typing_extensions import override + from BaseClasses import ItemClassification, LocationProgressType, \ 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 +22,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 @@ -48,7 +48,7 @@ class RomStart(str): """ rom_file: RomFile = RomFile(RomFile.copy_to) - rom_start: typing.Union[RomStart, bool] = RomStart("retroarch") + rom_start: RomStart | bool = RomStart("retroarch") class ZillionWebWorld(WebWorld): @@ -77,7 +77,7 @@ class ZillionWorld(World): options_dataclass = ZillionOptions options: ZillionOptions # type: ignore - settings: typing.ClassVar[ZillionSettings] # type: ignore + settings: ClassVar[ZillionSettings] # type: ignore # these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486 topology_present = True # indicate if world type has any meaningful layout/pathing @@ -90,14 +90,14 @@ class ZillionWorld(World): class LogStreamInterface: logger: logging.Logger - buffer: List[str] + buffer: list[str] def __init__(self, logger: logging.Logger) -> None: self.logger = logger self.buffer = [] def write(self, msg: str) -> None: - if msg.endswith('\n'): + if msg.endswith("\n"): self.buffer.append(msg[:-1]) self.logger.debug("".join(self.buffer)) self.buffer = [] @@ -109,20 +109,21 @@ def flush(self) -> None: lsi: LogStreamInterface - id_to_zz_item: Optional[Dict[int, ZzItem]] = None + id_to_zz_item: dict[int, ZzItem] | None = None zz_system: System - _item_counts: "Counter[str]" = Counter() + _item_counts: Counter[str] = Counter() """ These are the items counts that will be in the game, which might be different from the item counts the player asked for in options (if the player asked for something invalid). """ - my_locations: List[ZillionLocation] = [] + my_locations: list[ZillionLocation] = [] """ 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: ZillionLogicCache | None = None - def __init__(self, world: MultiWorld, player: int): + def __init__(self, world: MultiWorld, player: int) -> None: super().__init__(world, player) self.logger = logging.getLogger("Zillion") self.lsi = ZillionWorld.LogStreamInterface(self.logger) @@ -133,10 +134,8 @@ def _make_item_maps(self, start_char: Chars) -> None: _id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char) self.id_to_zz_item = id_to_zz_item + @override 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: @@ -145,24 +144,27 @@ def generate_early(self) -> None: self._item_counts = item_counts with redirect_stdout(self.lsi): # type: ignore - self.zz_system.make_randomizer(zz_op) - - self.zz_system.seed(self.multiworld.seed) + self.zz_system.set_options(zz_op) + self.zz_system.seed(self.random.randrange(1999999999)) self.zz_system.make_map() + self.zz_system.make_randomizer() # just in case the options changed anything (I don't think they do) assert self.zz_system.randomizer, "init failed" for zz_name in self.zz_system.randomizer.locations: - if zz_name != 'main': + if zz_name != "main": assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \ f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map" self._make_item_maps(zz_op.start_char) + @override 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 = [] @@ -178,38 +180,35 @@ def create_regions(self) -> None: zz_loc.req.gun = 1 assert len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) != 0 - start = self.zz_system.randomizer.regions['start'] + start = self.zz_system.randomizer.regions["start"] - all: Dict[str, ZillionRegion] = {} + all_regions: dict[str, ZillionRegion] = {} for here_zz_name, zz_r in self.zz_system.randomizer.regions.items(): here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name) - all[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w) - self.multiworld.regions.append(all[here_name]) + all_regions[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w) + self.multiworld.regions.append(all_regions[here_name]) limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126) queue = deque([start]) - done: Set[str] = set() + done: set[str] = set() while len(queue): zz_here = queue.popleft() here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name) if here_name in done: continue - here = all[here_name] + here = all_regions[here_name] for zz_loc in zz_here.locations: # if local gun reqs didn't place "keyword" item 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) @@ -221,15 +220,16 @@ def access_rule_wrapped(zz_loc_local: ZzLocation, self.my_locations.append(loc) for zz_dest in zz_here.connections.keys(): - dest_name = "Menu" if zz_dest.name == 'start' else zz_reg_name_to_reg_name(zz_dest.name) - dest = all[dest_name] - exit = Entrance(p, f"{here_name} to {dest_name}", here) - here.exits.append(exit) - exit.connect(dest) + dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name) + dest = all_regions[dest_name] + exit_ = Entrance(p, f"{here_name} to {dest_name}", here) + here.exits.append(exit_) + exit_.connect(dest) queue.append(zz_dest) done.add(here.name) + @override def create_items(self) -> None: if not self.id_to_zz_item: self._make_item_maps("JJ") @@ -253,14 +253,11 @@ def create_items(self) -> None: self.logger.debug(f"Zillion Items: {item_name} 1") self.multiworld.itempool.append(self.create_item(item_name)) - def set_rules(self) -> None: - # logic for this game is in create_regions - pass - + @override def generate_basic(self) -> None: assert self.zz_system.randomizer, "generate_early hasn't been called" # main location name is an alias - main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations['main'].name] + main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations["main"].name] self.multiworld.get_location(main_loc_name, self.player)\ .place_locked_item(self.create_item("Win")) @@ -268,22 +265,18 @@ def generate_basic(self) -> None: lambda state: state.has("Win", self.player) @staticmethod - def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: + def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: # noqa: ANN401 # item link pools are about to be created in main # JJ can't be an item link unless all the players share the same start_char # (The reason for this is that the JJ ZillionItem will have a different ZzItem depending # on whether the start char is Apple or Champ, and the logic depends on that ZzItem.) for group in multiworld.groups.values(): - # TODO: remove asserts on group when we can specify which members of TypedDict are optional - assert "game" in group - if group["game"] == "Zillion": - assert "item_pool" in group + if group["game"] == "Zillion" and "item_pool" in group: item_pool = group["item_pool"] to_stay: Chars = "JJ" if "JJ" in item_pool: - assert "players" in group - group_players = group["players"] - players_start_chars: List[Tuple[int, Chars]] = [] + group["players"] = group_players = set(group["players"]) + players_start_chars: list[tuple[int, Chars]] = [] for player in group_players: z_world = multiworld.worlds[player] assert isinstance(z_world, ZillionWorld) @@ -295,17 +288,17 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: elif start_char_counts["Champ"] > start_char_counts["Apple"]: to_stay = "Champ" else: # equal - choices: Tuple[Chars, ...] = ("Apple", "Champ") + choices: tuple[Chars, ...] = ("Apple", "Champ") to_stay = multiworld.random.choice(choices) for p, sc in players_start_chars: if sc != to_stay: group_players.remove(p) - assert "world" in group group_world = group["world"] assert isinstance(group_world, ZillionWorld) group_world._make_item_maps(to_stay) + @override def post_fill(self) -> None: """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. This happens before progression balancing, so the items may not be in their final locations yet.""" @@ -321,10 +314,10 @@ def finalize_item_locations(self) -> GenData: assert self.zz_system.randomizer, "generate_early hasn't been called" - # debug_zz_loc_ids: Dict[str, int] = {} + # debug_zz_loc_ids: dict[str, int] = {} empty = zz_items[4] multi_item = empty # a different patcher method differentiates empty from ap multi item - multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) + multi_items: dict[str, tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) for z_loc in self.multiworld.get_locations(self.player): assert isinstance(z_loc, ZillionLocation) # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) @@ -347,7 +340,7 @@ def finalize_item_locations(self) -> GenData: # print(id_) # print("size:", len(debug_zz_loc_ids)) - # debug_loc_to_id: Dict[str, int] = {} + # debug_loc_to_id: dict[str, int] = {} # regions = self.zz_randomizer.regions # for region in regions.values(): # for loc in region.locations: @@ -362,10 +355,11 @@ def finalize_item_locations(self) -> GenData: f"in world {self.player} didn't get an item" ) - game_id = self.multiworld.player_name[self.player].encode() + b'\x00' + self.multiworld.seed_name[-6:].encode() + game_id = self.multiworld.player_name[self.player].encode() + b"\x00" + self.multiworld.seed_name[-6:].encode() return GenData(multi_items, self.zz_system.get_game(), game_id) + @override def generate_output(self, output_directory: str) -> None: """This method gets called from a threadpool, do not use multiworld.random here. If you need any last-second randomization, use self.random instead.""" @@ -387,6 +381,7 @@ def generate_output(self, output_directory: str) -> None: self.logger.debug(f"Zillion player {self.player} finished generate_output") + @override def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot """Fill in the `slot_data` field in the `Connected` network package. This is a way the generator can give custom data to the client. @@ -402,15 +397,9 @@ 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 + @override def create_item(self, name: str) -> Item: """Create an item for this world type and player. Warning: this may be called with self.multiworld = None, for example by MultiServer""" @@ -431,6 +420,7 @@ def create_item(self, name: str) -> Item: z_item = ZillionItem(name, classification, item_id, self.player, zz_item) return z_item + @override def get_filler_item_name(self) -> str: """Called when the item pool needs to be filled with additional items to match location count.""" return "Empty" diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py index be32028463c7..d629df583a81 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -3,7 +3,7 @@ import io import pkgutil import platform -from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast +from typing import Any, ClassVar, Coroutine, Protocol, cast from CommonClient import CommonContext, server_loop, gui_enabled, \ ClientCommandProcessor, logger, get_base_parser @@ -11,6 +11,7 @@ from Utils import async_start import colorama +from typing_extensions import override from zilliandomizer.zri.memory import Memory, RescueInfo from zilliandomizer.zri import events @@ -35,11 +36,11 @@ def _cmd_map(self) -> None: class ToggleCallback(Protocol): - def __call__(self) -> None: ... + def __call__(self) -> object: ... class SetRoomCallback(Protocol): - def __call__(self, rooms: List[List[int]]) -> None: ... + def __call__(self, rooms: list[list[int]]) -> object: ... class ZillionContext(CommonContext): @@ -47,7 +48,7 @@ class ZillionContext(CommonContext): command_processor = ZillionCommandProcessor items_handling = 1 # receive items from other players - known_name: Optional[str] + known_name: str | None """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ from_game: "asyncio.Queue[events.EventFromGame]" @@ -56,11 +57,11 @@ class ZillionContext(CommonContext): """ local checks watched by server """ next_item: int """ index in `items_received` """ - ap_id_to_name: Dict[int, str] - ap_id_to_zz_id: Dict[int, int] + ap_id_to_name: dict[int, str] + ap_id_to_zz_id: dict[int, int] start_char: Chars = "JJ" - rescues: Dict[int, RescueInfo] = {} - loc_mem_to_id: Dict[int, int] = {} + rescues: dict[int, RescueInfo] = {} + loc_mem_to_id: dict[int, int] = {} got_room_info: asyncio.Event """ flag for connected to server """ got_slot_data: asyncio.Event @@ -119,22 +120,22 @@ def reset_game_state(self) -> None: self.finished_game = False self.items_received.clear() - # override - def on_deathlink(self, data: Dict[str, Any]) -> None: + @override + def on_deathlink(self, data: dict[str, Any]) -> None: self.to_game.put_nowait(events.DeathEventToGame()) return super().on_deathlink(data) - # override + @override async def server_auth(self, password_requested: bool = False) -> None: if password_requested and not self.password: await super().server_auth(password_requested) if not self.auth: - logger.info('waiting for connection to game...') + logger.info("waiting for connection to game...") return logger.info("logging in to server...") await self.send_connect() - # override + @override def run_gui(self) -> None: from kvui import GameManager from kivy.core.text import Label as CoreLabel @@ -154,10 +155,10 @@ class MapPanel(Widget): MAP_WIDTH: ClassVar[int] = 281 map_background: CoreImage - _number_textures: List[Texture] = [] - rooms: List[List[int]] = [] + _number_textures: list[Texture] = [] + rooms: list[list[int]] = [] - def __init__(self, **kwargs: Any) -> None: + def __init__(self, **kwargs: Any) -> None: # noqa: ANN401 super().__init__(**kwargs) FILE_NAME = "empty-zillion-map-row-col-labels-281.png" @@ -183,7 +184,7 @@ def _make_numbers(self) -> None: label.refresh() self._number_textures.append(label.texture) - def update_map(self, *args: Any) -> None: + def update_map(self, *args: Any) -> None: # noqa: ANN401 self.canvas.clear() with self.canvas: @@ -203,6 +204,7 @@ def update_map(self, *args: Any) -> None: num_texture = self._number_textures[num] Rectangle(texture=num_texture, size=num_texture.size, pos=pos) + @override def build(self) -> Layout: container = super().build() self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=ZillionManager.MapPanel.MAP_WIDTH) @@ -216,17 +218,18 @@ def toggle_map_width(self) -> None: self.map_widget.width = 0 self.container.do_layout() - def set_rooms(self, rooms: List[List[int]]) -> None: + def set_rooms(self, rooms: list[list[int]]) -> None: self.map_widget.rooms = rooms self.map_widget.update_map() self.ui = ZillionManager(self) - self.ui_toggle_map = lambda: self.ui.toggle_map_width() - self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) + self.ui_toggle_map = lambda: isinstance(self.ui, ZillionManager) and self.ui.toggle_map_width() + self.ui_set_rooms = lambda rooms: isinstance(self.ui, ZillionManager) and self.ui.set_rooms(rooms) run_co: Coroutine[Any, Any, None] = self.ui.async_run() self.ui_task = asyncio.create_task(run_co, name="UI") - def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + @override + def on_package(self, cmd: str, args: dict[str, Any]) -> None: self.room_item_numbers_to_ui() if cmd == "Connected": logger.info("logged in to Archipelago server") @@ -238,7 +241,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None: if "start_char" not in slot_data: logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") return - self.start_char = slot_data['start_char'] + self.start_char = slot_data["start_char"] if self.start_char not in {"Apple", "Champ", "JJ"}: logger.warning("invalid Zillion `Connected` packet, " f"`slot_data` `start_char` has invalid value: {self.start_char}") @@ -259,7 +262,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None: self.rescues[0 if rescue_id == "0" else 1] = ri if "loc_mem_to_id" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") return loc_mem_to_id = slot_data["loc_mem_to_id"] self.loc_mem_to_id = {} @@ -286,7 +289,7 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None: if "keys" not in args: logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") return - keys = cast(Dict[str, Optional[str]], args["keys"]) + keys = cast(dict[str, str | None], args["keys"]) doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) if doors_b64: logger.info("received door data from server") @@ -321,9 +324,9 @@ def process_from_game_queue(self) -> None: if server_id in self.missing_locations: self.ap_local_count += 1 n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win - logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + logger.info(f"New Check: {loc_name} ({self.ap_local_count}/{n_locations})") async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [server_id]} + {"cmd": "LocationChecks", "locations": [server_id]} ])) else: # This will happen a lot in Zillion, @@ -334,7 +337,7 @@ def process_from_game_queue(self) -> None: elif isinstance(event_from_game, events.WinEventFromGame): if not self.finished_game: async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [loc_name_to_id["J-6 bottom far left"]]}, + {"cmd": "LocationChecks", "locations": [loc_name_to_id["J-6 bottom far left"]]}, {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} ])) self.finished_game = True @@ -347,6 +350,11 @@ def process_from_game_queue(self) -> None: "operations": [{"operation": "replace", "value": doors_b64}] } async_start(self.send_msgs([payload])) + elif isinstance(event_from_game, events.MapEventFromGame): + row = event_from_game.map_index // 8 + col = event_from_game.map_index % 8 + room_name = f"({chr(row + 64)}-{col + 1})" + logger.info(f"You are at {room_name}") else: logger.warning(f"WARNING: unhandled event from game {event_from_game}") @@ -357,24 +365,24 @@ def process_items_received(self) -> None: ap_id = self.items_received[index].item from_name = self.player_names[self.items_received[index].player] # TODO: colors in this text, like sni client? - logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + logger.info(f"received {self.ap_id_to_name[ap_id]} from {from_name}") self.to_game.put_nowait( events.ItemEventToGame(zz_item_ids) ) self.next_item = len(self.items_received) -def name_seed_from_ram(data: bytes) -> Tuple[str, str]: +def name_seed_from_ram(data: bytes) -> tuple[str, str]: """ returns player name, and end of seed string """ if len(data) == 0: # no connection to game return "", "xxx" - null_index = data.find(b'\x00') + null_index = data.find(b"\x00") if null_index == -1: logger.warning(f"invalid game id in rom {repr(data)}") null_index = len(data) name = data[:null_index].decode() - null_index_2 = data.find(b'\x00', null_index + 1) + null_index_2 = data.find(b"\x00", null_index + 1) if null_index_2 == -1: null_index_2 = len(data) seed_name = data[null_index + 1:null_index_2].decode() @@ -474,8 +482,8 @@ def log_no_spam(msg: str) -> None: async def main() -> None: parser = get_base_parser() - parser.add_argument('diff_file', default="", type=str, nargs="?", - help='Path to a .apzl Archipelago Binary Patch file') + parser.add_argument("diff_file", default="", type=str, nargs="?", + help="Path to a .apzl Archipelago Binary Patch file") # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) args = parser.parse_args() print(args) diff --git a/worlds/zillion/gen_data.py b/worlds/zillion/gen_data.py index aa24ff8961b3..214073396153 100644 --- a/worlds/zillion/gen_data.py +++ b/worlds/zillion/gen_data.py @@ -1,6 +1,5 @@ from dataclasses import dataclass import json -from typing import Dict, Tuple from zilliandomizer.game import Game as ZzGame @@ -9,7 +8,7 @@ class GenData: """ data passed from generation to patcher """ - multi_items: Dict[str, Tuple[str, str]] + multi_items: dict[str, tuple[str, str]] """ zz_loc_name to (item_name, player_name) """ zz_game: ZzGame game_id: bytes @@ -28,6 +27,13 @@ def to_json(self) -> str: def from_json(gen_data_str: str) -> "GenData": """ the reverse of `to_json` """ from_json = json.loads(gen_data_str) + + # backwards compatibility for seeds generated before new map_gen options + room_gen = from_json["zz_game"]["options"].get("room_gen", None) + if room_gen is not None: + from_json["zz_game"]["options"]["map_gen"] = {False: "none", True: "rooms"}.get(room_gen, "none") + del from_json["zz_game"]["options"]["room_gen"] + return GenData( from_json["multi_items"], ZzGame.from_jsonable(from_json["zz_game"]), diff --git a/worlds/zillion/id_maps.py b/worlds/zillion/id_maps.py index 32d71fc79b30..25762f99cd6b 100644 --- a/worlds/zillion/id_maps.py +++ b/worlds/zillion/id_maps.py @@ -1,5 +1,6 @@ from collections import defaultdict -from typing import Dict, Iterable, Mapping, Tuple, TypedDict +from collections.abc import Iterable, Mapping +from typing import TypedDict from zilliandomizer.logic_components.items import ( Item as ZzItem, @@ -40,13 +41,13 @@ _zz_empty = zz_item_name_to_zz_item["empty"] -def make_id_to_others(start_char: Chars) -> Tuple[ - Dict[int, str], Dict[int, int], Dict[int, ZzItem] +def make_id_to_others(start_char: Chars) -> tuple[ + dict[int, str], dict[int, int], dict[int, ZzItem] ]: """ returns id_to_name, id_to_zz_id, id_to_zz_item """ - id_to_name: Dict[int, str] = {} - id_to_zz_id: Dict[int, int] = {} - id_to_zz_item: Dict[int, ZzItem] = {} + id_to_name: dict[int, str] = {} + id_to_zz_id: dict[int, int] = {} + id_to_zz_item: dict[int, ZzItem] = {} if start_char == "JJ": name_to_zz_item = { @@ -91,14 +92,14 @@ def make_room_name(row: int, col: int) -> str: return f"{chr(ord('A') + row - 1)}-{col + 1}" -loc_name_to_id: Dict[str, int] = { +loc_name_to_id: dict[str, int] = { name: id_ + base_id for name, id_ in pretty_loc_name_to_id.items() } def zz_reg_name_to_reg_name(zz_reg_name: str) -> str: - if zz_reg_name[0] == 'r' and zz_reg_name[3] == 'c': + if zz_reg_name[0] == "r" and zz_reg_name[3] == "c": row, col = parse_reg_name(zz_reg_name) end = zz_reg_name[5:] return f"{make_room_name(row, col)} {end.upper()}" @@ -113,17 +114,17 @@ class ClientRescue(TypedDict): class ZillionSlotInfo(TypedDict): start_char: Chars - rescues: Dict[str, ClientRescue] - loc_mem_to_id: Dict[int, int] + rescues: dict[str, ClientRescue] + loc_mem_to_id: dict[int, int] """ memory location of canister to Archipelago location id number """ def get_slot_info(regions: Iterable[RegionData], start_char: Chars, loc_name_to_pretty: Mapping[str, str]) -> ZillionSlotInfo: - items_placed_in_map_index: Dict[int, int] = defaultdict(int) - rescue_locations: Dict[int, RescueInfo] = {} - loc_memory_to_loc_id: Dict[int, int] = {} + items_placed_in_map_index: dict[int, int] = defaultdict(int) + rescue_locations: dict[int, RescueInfo] = {} + loc_memory_to_loc_id: dict[int, int] = {} for region in regions: for loc in region.locations: assert loc.item, ("There should be an item placed in every location before " @@ -142,7 +143,7 @@ def get_slot_info(regions: Iterable[RegionData], loc_memory_to_loc_id[loc_memory] = pretty_loc_name_to_id[loc_name_to_pretty[loc.name]] items_placed_in_map_index[map_index] += 1 - rescues: Dict[str, ClientRescue] = {} + rescues: dict[str, ClientRescue] = {} for i in (0, 1): if i in rescue_locations: ri = rescue_locations[i] diff --git a/worlds/zillion/logic.py b/worlds/zillion/logic.py index dcbc6131f1a9..f3d1814a9e9b 100644 --- a/worlds/zillion/logic.py +++ b/worlds/zillion/logic.py @@ -1,4 +1,5 @@ -from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter +from collections import Counter +from collections.abc import Mapping from BaseClasses import CollectionState @@ -35,7 +36,7 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int: return _hash -def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]: +def item_counts(cs: CollectionState, p: int) -> tuple[tuple[str, int], ...]: """ the zilliandomizer items that player p has collected @@ -44,38 +45,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 diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index d75dd1a1c22c..22a698472265 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,9 +1,8 @@ from collections import Counter from dataclasses import dataclass -from typing import ClassVar, Dict, Tuple -from typing_extensions import TypeGuard # remove when Python >= 3.10 +from typing import ClassVar, Literal, TypeGuard -from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Toggle +from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle from zilliandomizer.options import ( Options as ZzOptions, char_to_gun, char_to_jump, ID, @@ -108,7 +107,7 @@ class ZillionStartChar(Choice): display_name = "start character" default = "random" - _name_capitalization: ClassVar[Dict[int, Chars]] = { + _name_capitalization: ClassVar[dict[int, Chars]] = { option_jj: "JJ", option_apple: "Apple", option_champ: "Champ", @@ -233,6 +232,7 @@ class ZillionSkill(Range): range_start = 0 range_end = 5 default = 2 + display_name = "skill" class ZillionStartingCards(NamedRange): @@ -251,9 +251,25 @@ class ZillionStartingCards(NamedRange): } -class ZillionRoomGen(Toggle): - """ whether to generate rooms with random terrain """ - display_name = "room generation" +class ZillionMapGen(Choice): + """ + - none: vanilla map + - rooms: random terrain inside rooms, but path through base is vanilla + - full: random path through base + """ + display_name = "map generation" + option_none = 0 + option_rooms = 1 + option_full = 2 + default = 0 + + def zz_value(self) -> Literal["none", "rooms", "full"]: + if self.value == ZillionMapGen.option_none: + return "none" + if self.value == ZillionMapGen.option_rooms: + return "rooms" + assert self.value == ZillionMapGen.option_full + return "full" @dataclass @@ -276,7 +292,9 @@ class ZillionOptions(PerGameCommonOptions): early_scope: ZillionEarlyScope skill: ZillionSkill starting_cards: ZillionStartingCards - room_gen: ZillionRoomGen + map_gen: ZillionMapGen + + room_gen: Removed z_option_groups = [ @@ -287,7 +305,7 @@ class ZillionOptions(PerGameCommonOptions): ] -def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: +def convert_item_counts(ic: Counter[str]) -> ZzItemCounts: tr: ZzItemCounts = { ID.card: ic["ID Card"], ID.red: ic["Red ID Card"], @@ -301,7 +319,7 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: return tr -def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": +def validate(options: ZillionOptions) -> tuple[ZzOptions, Counter[str]]: """ adjusts options to make game completion possible @@ -375,7 +393,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": starting_cards = options.starting_cards - room_gen = options.room_gen + map_gen = options.map_gen.zz_value() zz_item_counts = convert_item_counts(item_counts) zz_op = ZzOptions( @@ -393,7 +411,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": bool(options.early_scope.value), True, # balance defense starting_cards.value, - bool(room_gen.value) + map_gen ) zz_validate(zz_op) return zz_op, item_counts diff --git a/worlds/zillion/patch.py b/worlds/zillion/patch.py index 6bc6d04dd663..0eee3315f4a1 100644 --- a/worlds/zillion/patch.py +++ b/worlds/zillion/patch.py @@ -1,5 +1,5 @@ import os -from typing import Any, BinaryIO, Optional, cast +from typing import BinaryIO import zipfile from typing_extensions import override @@ -11,11 +11,11 @@ from .gen_data import GenData -USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270' +US_HASH = "d4bf9e7bcf9a48da53785d2ae7bc4270" class ZillionPatch(APAutoPatchInterface): - hash = USHASH + hash = US_HASH game = "Zillion" patch_file_ending = ".apzl" result_file_ending = ".sms" @@ -23,8 +23,14 @@ class ZillionPatch(APAutoPatchInterface): gen_data_str: str """ JSON encoded """ - def __init__(self, *args: Any, gen_data_str: str = "", **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + def __init__(self, + path: str | None = None, + player: int | None = None, + player_name: str = "", + server: str = "", + *, + gen_data_str: str = "") -> None: + super().__init__(path=path, player=player, player_name=player_name, server=server) self.gen_data_str = gen_data_str @classmethod @@ -44,15 +50,17 @@ def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None: super().read_contents(opened_zipfile) self.gen_data_str = opened_zipfile.read("gen_data.json").decode() + @override def patch(self, target: str) -> None: self.read() write_rom_from_gen_data(self.gen_data_str, target) -def get_base_rom_path(file_name: Optional[str] = None) -> str: - options = Utils.get_options() +def get_base_rom_path(file_name: str | None = None) -> str: + from . import ZillionSettings, ZillionWorld + settings: ZillionSettings = ZillionWorld.settings if not file_name: - file_name = cast(str, options["zillion_options"]["rom_file"]) + file_name = settings.rom_file if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/zillion/region.py b/worlds/zillion/region.py index cf5aa6588950..40565f008263 100644 --- a/worlds/zillion/region.py +++ b/worlds/zillion/region.py @@ -1,9 +1,11 @@ -from typing import Optional -from BaseClasses import MultiWorld, Region, Location, Item, CollectionState +from typing_extensions import override + from zilliandomizer.logic_components.regions import Region as ZzRegion from zilliandomizer.logic_components.locations import Location as ZzLocation from zilliandomizer.logic_components.items import RESCUE +from BaseClasses import MultiWorld, Region, Location, Item, CollectionState + from .id_maps import loc_name_to_id from .item import ZillionItem @@ -28,12 +30,12 @@ def __init__(self, zz_loc: ZzLocation, player: int, name: str, - parent: Optional[Region] = None) -> None: + parent: Region | None = None) -> None: loc_id = loc_name_to_id[name] super().__init__(player, name, loc_id, parent) self.zz_loc = zz_loc - # override + @override def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool: saved_gun_req = -1 if isinstance(item, ZillionItem) \ diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index ae7d9b173308..d6b01ac107ae 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@1dd2ce01c9d818caba5844529699b3ad026d6a07#0.7.1 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0 typing-extensions>=4.7, <5 diff --git a/worlds/zillion/test/TestReproducibleRandom.py b/worlds/zillion/test/TestReproducibleRandom.py index a92fae240709..352165449a8b 100644 --- a/worlds/zillion/test/TestReproducibleRandom.py +++ b/worlds/zillion/test/TestReproducibleRandom.py @@ -1,4 +1,3 @@ -from typing import cast from . import ZillionTestBase from .. import ZillionWorld @@ -9,7 +8,8 @@ class SeedTest(ZillionTestBase): def test_reproduce_seed(self) -> None: self.world_setup(42) - z_world = cast(ZillionWorld, self.multiworld.worlds[1]) + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) r = z_world.zz_system.randomizer assert r randomized_requirements_first = tuple( @@ -18,7 +18,8 @@ def test_reproduce_seed(self) -> None: ) self.world_setup(42) - z_world = cast(ZillionWorld, self.multiworld.worlds[1]) + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) r = z_world.zz_system.randomizer assert r randomized_requirements_second = tuple( diff --git a/worlds/zillion/test/__init__.py b/worlds/zillion/test/__init__.py index fe62bae34c9e..a669442364fe 100644 --- a/worlds/zillion/test/__init__.py +++ b/worlds/zillion/test/__init__.py @@ -1,4 +1,3 @@ -from typing import cast from test.bases import WorldTestBase from .. import ZillionWorld @@ -13,8 +12,9 @@ def ensure_gun_3_requirement(self) -> None: This makes sure that gun 3 is required by making all the canisters in O-7 (including key word canisters) require gun 3. """ - zz_world = cast(ZillionWorld, self.multiworld.worlds[1]) - assert zz_world.zz_system.randomizer - for zz_loc_name, zz_loc in zz_world.zz_system.randomizer.locations.items(): + z_world = self.multiworld.worlds[1] + assert isinstance(z_world, ZillionWorld) + assert z_world.zz_system.randomizer + for zz_loc_name, zz_loc in z_world.zz_system.randomizer.locations.items(): if zz_loc_name.startswith("r15c6"): zz_loc.req.gun = 3